diff --git a/.github/actions/upload-coverage/action.yml b/.github/actions/upload-coverage/action.yml new file mode 100644 index 000000000..64e1fca90 --- /dev/null +++ b/.github/actions/upload-coverage/action.yml @@ -0,0 +1,22 @@ +name: Upload Coverage +description: Upload coverage files + +runs: + using: "composite" + + steps: + - run: | + COVERAGE_UUID=$(python3 -c "import uuid; print(uuid.uuid4())") + echo "COVERAGE_UUID=${COVERAGE_UUID}" >> $GITHUB_OUTPUT + if [ -f .coverage ]; then + mv .coverage .coverage.${COVERAGE_UUID} + fi + id: coverage-uuid + shell: bash + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-data-${{ steps.coverage-uuid.outputs.COVERAGE_UUID }} + path: | + .coverage.* + if-no-files-found: ignore + include-hidden-files: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..317291fdb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directories: + - "/" + - "/.github/actions/*/" + schedule: + interval: "daily" + cooldown: + default-days: 7 diff --git a/.github/downstream.d/certbot-josepy.sh b/.github/downstream.d/certbot-josepy.sh new file mode 100755 index 000000000..f172dd008 --- /dev/null +++ b/.github/downstream.d/certbot-josepy.sh @@ -0,0 +1,20 @@ +#!/bin/bash -ex + +case "${1}" in + install) + git clone --depth=1 https://github.com/certbot/josepy + cd josepy + git rev-parse HEAD + curl -sSL https://install.python-poetry.org | python3 - + "${HOME}/.local/bin/poetry" self add poetry-plugin-export + "${HOME}/.local/bin/poetry" export -f constraints.txt --dev --without-hashes -o constraints.txt + pip install -e . pytest -c constraints.txt + ;; + run) + cd josepy + pytest tests + ;; + *) + exit 1 + ;; +esac diff --git a/.github/downstream.d/certbot.sh b/.github/downstream.d/certbot.sh new file mode 100755 index 000000000..561251d5b --- /dev/null +++ b/.github/downstream.d/certbot.sh @@ -0,0 +1,23 @@ +#!/bin/bash -ex + +case "${1}" in + install) + git clone --depth=1 https://github.com/certbot/certbot + cd certbot + git rev-parse HEAD + tools/pip_install.py -e ./acme[test] + tools/pip_install.py -e ./certbot[test] + pip install -U pyopenssl + ;; + run) + cd certbot + # Ignore some warnings for now since they're now automatically promoted + # to errors. We can probably remove this when acme gets split into + # its own repo + pytest -Wignore certbot + pytest acme + ;; + *) + exit 1 + ;; +esac diff --git a/.github/downstream.d/twisted.sh b/.github/downstream.d/twisted.sh new file mode 100755 index 000000000..9fc195ba7 --- /dev/null +++ b/.github/downstream.d/twisted.sh @@ -0,0 +1,17 @@ +#!/bin/bash -ex + +case "${1}" in + install) + git clone --depth=1 https://github.com/twisted/twisted + cd twisted + git rev-parse HEAD + pip install ".[all_non_platform]" + ;; + run) + cd twisted + python -m twisted.trial -j4 src/twisted + ;; + *) + exit 1 + ;; +esac diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..ad4658f3e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,183 @@ +name: CI +on: + pull_request: {} + push: {} + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.PYTHON.OS || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + PYTHON: + # Base builds + - {VERSION: "3.9", NOXSESSION: "tests"} + - {VERSION: "3.10", NOXSESSION: "tests"} + - {VERSION: "3.11", NOXSESSION: "tests"} + - {VERSION: "3.12", NOXSESSION: "tests"} + - {VERSION: "3.13", NOXSESSION: "tests"} + - {VERSION: "3.14", NOXSESSION: "tests"} + - {VERSION: "3.14t", NOXSESSION: "tests"} + - {VERSION: "pypy-3.11", NOXSESSION: "tests"} + - {VERSION: "3.11", NOXSESSION: "tests-wheel", OS: "windows-latest"} + - {VERSION: "3.14t", NOXSESSION: "tests-wheel", OS: "windows-latest"} + # -cryptography-main + - {VERSION: "3.9", NOXSESSION: "tests-cryptography-main"} + - {VERSION: "3.10", NOXSESSION: "tests-cryptography-main"} + - {VERSION: "3.11", NOXSESSION: "tests-cryptography-main"} + - {VERSION: "3.12", NOXSESSION: "tests-cryptography-main"} + - {VERSION: "3.13", NOXSESSION: "tests-cryptography-main"} + - {VERSION: "3.14", NOXSESSION: "tests-cryptography-main"} + - {VERSION: "3.14t", NOXSESSION: "tests-cryptography-main"} + - {VERSION: "pypy-3.11", NOXSESSION: "tests-cryptography-main"} + # -cryptography-minimum + - {VERSION: "3.9", NOXSESSION: "tests-cryptography-minimum"} + - {VERSION: "3.10", NOXSESSION: "tests-cryptography-minimum"} + - {VERSION: "3.11", NOXSESSION: "tests-cryptography-minimum"} + - {VERSION: "3.12", NOXSESSION: "tests-cryptography-minimum"} + - {VERSION: "3.13", NOXSESSION: "tests-cryptography-minimum"} + - {VERSION: "pypy-3.11", NOXSESSION: "tests-cryptography-minimum"} + # Cryptography wheels + - {VERSION: "3.9", NOXSESSION: "tests-cryptography-minimum-wheel"} + - {VERSION: "3.9", NOXSESSION: "tests-wheel"} + # Random order + - {VERSION: "3.9", NOXSESSION: "tests-random-order"} + # aws-lc + - {VERSION: "3.14", NOXSESSION: "tests-cryptography-main", OPENSSL: {TYPE: "aws-lc", VERSION: "v1.73.0"}} + # Meta + - {VERSION: "3.9", NOXSESSION: "check-manifest"} + - {VERSION: "3.11", NOXSESSION: "lint"} + - {VERSION: "3.13", NOXSESSION: "mypy"} + - {VERSION: "3.9", NOXSESSION: "docs"} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Setup python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ matrix.PYTHON.VERSION }} + - name: Cache custom OpenSSL + if: matrix.PYTHON.OPENSSL + id: ossl-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/ossl + key: ossl-${{ matrix.PYTHON.OPENSSL.TYPE }}-${{ matrix.PYTHON.OPENSSL.VERSION }} + - name: Build custom OpenSSL + if: matrix.PYTHON.OPENSSL && steps.ossl-cache.outputs.cache-hit != 'true' + run: | + git clone --depth 1 --branch ${{ matrix.PYTHON.OPENSSL.VERSION }} https://github.com/aws/aws-lc.git + cmake -GNinja -B aws-lc/build -S aws-lc -DCMAKE_INSTALL_PREFIX="${HOME}/ossl" -DBUILD_TESTING=OFF + ninja -C aws-lc/build install + rm -rf aws-lc/ + - name: Set up custom OpenSSL env + if: matrix.PYTHON.OPENSSL + run: | + cargo install bindgen-cli + echo "OPENSSL_DIR=${HOME}/ossl" >> $GITHUB_ENV + - run: python -m pip install nox + - run: nox + env: + NOXSESSION: ${{ matrix.PYTHON.NOXSESSION }} + - uses: ./.github/actions/upload-coverage + + linux-docker: + runs-on: ubuntu-latest + container: ghcr.io/pyca/cryptography-runner-${{ matrix.TEST.CONTAINER }} + strategy: + fail-fast: false + matrix: + TEST: + # cryptography-main used since there's no wheel + - {CONTAINER: "ubuntu-rolling", NOXSESSION: "tests-cryptography-main"} + name: "${{ matrix.TEST.NOXSESSION }} on ${{ matrix.TEST.CONTAINER }}" + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - run: /venv/bin/pip install nox + - run: /venv/bin/nox + env: + RUSTUP_HOME: /root/.rustup + NOXSESSION: ${{ matrix.TEST.NOXSESSION }} + - uses: ./.github/actions/upload-coverage + + linux-downstream: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + DOWNSTREAM: + - twisted + - certbot + - certbot-josepy + PYTHON: + - 3.12 + name: "Downstream tests for ${{ matrix.DOWNSTREAM }}" + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Setup python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ matrix.PYTHON }} + - run: ./.github/downstream.d/${{ matrix.DOWNSTREAM }}.sh install + - run: pip install . + - run: ./.github/downstream.d/${{ matrix.DOWNSTREAM }}.sh run + + all-green: + runs-on: ubuntu-latest + needs: [linux, linux-docker, linux-downstream] + if: ${{ always() }} + timeout-minutes: 3 + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + jobs: ${{ toJSON(needs) }} + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + timeout-minutes: 3 + with: + persist-credentials: false + - name: Setup python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: '3.12' + timeout-minutes: 3 + - run: pip install coverage[toml] + - name: Download coverage data + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: coverage-data-* + merge-multiple: true + - name: Combine coverage and fail if it's too low + id: combinecoverage + run: | + set +e + python -m coverage combine + echo "## Python Coverage" >> $GITHUB_STEP_SUMMARY + python -m coverage report -m --fail-under=98 > COV_REPORT + COV_EXIT_CODE=$? + cat COV_REPORT + if [ $COV_EXIT_CODE -ne 0 ]; then + echo "🚨 Python Coverage failed. Coverage too low." | tee -a $GITHUB_STEP_SUMMARY + fi + echo '```' >> $GITHUB_STEP_SUMMARY + cat COV_REPORT >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit $COV_EXIT_CODE + - name: Create coverage HTML + run: python -m coverage html + if: ${{ failure() && steps.combinecoverage.outcome == 'failure' }} + - name: Upload HTML report. + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: _html-report + path: htmlcov + if-no-files-found: ignore diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 000000000..a05c798a1 --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,17 @@ +name: Lock Issues +on: + schedule: + - cron: '0 0 * * *' + +permissions: + issues: "write" + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + issue-inactive-days: 90 + pr-inactive-days: 90 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..664389992 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +on: + workflow_dispatch: + push: + tags: + - "*.*.*" + +name: release + +permissions: + contents: read + +jobs: + build: + name: Build distributions for PyPI + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + + - name: Install build dependencies + run: python -m pip install uv + + - name: Build distributions + run: python -m uv build + + - name: Upload distributions + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pyopenssl-dists + path: dist/ + + pypi: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: + - build + + permissions: + # Used to authenticate to PyPI via OIDC. + id-token: write + + steps: + - name: fetch dists + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pyopenssl-dists + path: dist/ + + - name: publish + if: github.event_name == 'push' + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + attestations: true diff --git a/.gitignore b/.gitignore index 7b60bb08c..edca32488 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,11 @@ dist *.pyc *.pyo __pycache__ -.tox +.nox +doc/_build/ +.coverage* +.eggs +examples/simple/*.cert +examples/simple/*.pkey +.cache +.mypy_cache \ No newline at end of file diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..10e5df792 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,12 @@ +version: 2 + +sphinx: + configuration: doc/conf.py + +build: + os: "ubuntu-24.04" + tools: + python: "3" + jobs: + post_install: + - pip install .[docs] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f359de1be..000000000 --- a/.travis.yml +++ /dev/null @@ -1,83 +0,0 @@ -language: python - -os: - - linux - -python: - - "pypy" - - "2.6" - - "2.7" - - "3.2" - - "3.3" - - "3.4" - -matrix: - include: - # Also run the tests against cryptography master. - - python: "2.6" - env: - CRYPTOGRAPHY_GIT_MASTER=true - - python: "2.7" - env: - CRYPTOGRAPHY_GIT_MASTER=true - - python: "3.2" - env: - CRYPTOGRAPHY_GIT_MASTER=true - - python: "3.3" - env: - CRYPTOGRAPHY_GIT_MASTER=true - - python: "3.4" - env: - CRYPTOGRAPHY_GIT_MASTER=true - - python: "pypy" - env: - CRYPTOGRAPHY_GIT_MASTER=true - - # Also run at least a little bit against an older version of OpenSSL. - - python: "2.7" - env: - OPENSSL=0.9.8 - - # Let the cryptography master builds fail because they might be triggered by - # cryptography changes beyond our control. - allow_failures: - - env: - CRYPTOGRAPHY_GIT_MASTER=true - - env: - OPENSSL=0.9.8 - -before_install: - - if [ -n "$CRYPTOGRAPHY_GIT_MASTER" ]; then pip install git+https://github.com/pyca/cryptography.git;fi - -install: - # Install the wheel library explicitly here. It is not really a setup - # dependency. It is not an install dependency. It is only a dependency for - # the script directive below - because we want to exercise wheel building on - # travis. - - pip install wheel - - # Also install some tools for measuring code coverage and sending the results - # to coveralls. - - pip install coveralls coverage - -script: - - | - if [[ "${OPENSSL}" == "0.9.8" ]]; then - sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ lucid main" - sudo apt-get -y update - sudo apt-get install -y --force-yes libssl-dev/lucid - fi - - | - pip install -e . - - | - coverage run --branch --source=OpenSSL setup.py bdist_wheel test - - | - coverage report -m - - | - python -c "import OpenSSL.SSL; print(OpenSSL.SSL.SSLeay_version(OpenSSL.SSL.SSLEAY_VERSION))" - -after_success: - - coveralls - -notifications: - email: false diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 000000000..6b694af20 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,884 @@ +Changelog +========= + +Versions are year-based with a strict backward-compatibility policy. +The third digit is only for regressions. + +26.3.0 (2026-06-12) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Dropped support for Python 3.8. +- The minimum ``cryptography`` version is now 49.0.0. +- Removed deprecated ``OpenSSL.crypto.X509Req``, ``OpenSSL.crypto.dump_certificate_request``, and ``OpenSSL.crypto.load_certificate_request``. ``cryptography.x509`` should be used instead. +- ``OpenSSL.SSL.Connection.set_session`` now raises ``ValueError`` if the ``Session`` was obtained from a ``Connection`` that was using a different ``Context`` than this one. OpenSSL requires (but does not verify) that sessions only be re-used with a compatible ``SSL_CTX``, so this contract is now enforced. + +Deprecations: +^^^^^^^^^^^^^ + +- Deprecated ``OpenSSL.crypto.PKey.generate_key`` and ``OpenSSL.crypto.PKey.check``. The key generation and loading APIs in ``cryptography`` should be used instead. +- Deprecated ``OpenSSL.crypto.dump_privatekey``. The serialization APIs on ``cryptography`` private key types should be used instead. +- Deprecated all the mutable APIs on ``OpenSSL.crypto.X509``: ``set_version``, ``set_pubkey``, ``sign``, ``set_serial_number``, ``gmtime_adj_notAfter``, ``gmtime_adj_notBefore``, ``set_notBefore``, ``set_notAfter``, ``set_issuer``, and ``set_subject``. ``cryptography.x509.CertificateBuilder`` should be used instead. +- Deprecated ``OpenSSL.SSL.Context.set_passwd_cb``. Users should decrypt and load their private keys themselves, with ``cryptography``'s key loading APIs, and then call ``OpenSSL.SSL.Context.use_privatekey``. +- Deprecated ``OpenSSL.crypto.X509Name``, as well as the remaining APIs that consume or return it: ``OpenSSL.crypto.X509.get_issuer``, ``OpenSSL.crypto.X509.get_subject``, and ``OpenSSL.SSL.Context.set_client_ca_list``. The APIs in ``cryptography.x509`` should be used instead. + +Changes: +^^^^^^^^ + +- ``OpenSSL.SSL.Connection.get_client_ca_list`` now takes an ``as_cryptography`` keyword-argument. When ``True`` is passed then ``cryptography.x509.Name`` are returned, instead of ``OpenSSL.crypto.X509Name``. In the future, passing ``False`` (the default) will be deprecated. + + +26.2.0 (2026-05-04) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Removed deprecated ``OpenSSL.crypto.X509Extension``, ``OpenSSL.crypto.X509Req.add_extension``, ``OpenSSL.crypto.X509Req.get_extensions``, ``OpenSSL.crypto.X509.add_extension``, ``OpenSSL.crypto.X509.get_extensions``. ``cryptography.x509`` should be used instead. +- It is now an error to calling any mutating method on ``OpenSSL.SSL.Context`` after it has been used to create a ``Connection``. This was previously deprecated and has always been unsafe. + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Maximum supported ``cryptography`` version is now 48.x. +- Added ``OpenSSL.SSL.Connection.set_options`` to set options on a per-connection basis. + +26.1.0 (2026-04-24) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Maximum supported ``cryptography`` version is now 47.x. +- Fixed ``X509Name`` field setters to correctly pass the value length to OpenSSL. Previously, values containing NUL bytes would be silently truncated, causing a divergence between the stored ASN.1 value and the value visible from Python. Credit to **BudongJW** for reporting the issue. **CVE-2026-40475** + +26.0.0 (2026-03-15) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Dropped support for Python 3.7. +- The minimum ``cryptography`` version is now 46.0.0. + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Added support for using aws-lc instead of OpenSSL. +- Properly raise an error if a DTLS cookie callback returned a cookie longer than ``DTLS1_COOKIE_LENGTH`` bytes. Previously this would result in a buffer-overflow. Credit to **dark_haxor** for reporting the issue. **CVE-2026-27459** +- Added ``OpenSSL.SSL.Connection.get_group_name`` to determine which group name was negotiated. +- ``Context.set_tlsext_servername_callback`` now handles exceptions raised in the callback by calling ``sys.excepthook`` and returning a fatal TLS alert. Previously, exceptions were silently swallowed and the handshake would proceed as if the callback had succeeded. Credit to **Leury Castillo** for reporting this issue. **CVE-2026-27448** + +25.3.0 (2025-09-16) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Maximum supported ``cryptography`` version is now 46.x. + + +25.2.0 (2025-09-14) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- The minimum ``cryptography`` version is now 45.0.7. + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- pyOpenSSL now sets ``SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER`` on connections by default, matching CPython's behavior. +- Added ``OpenSSL.SSL.Context.clear_mode``. +- Added ``OpenSSL.SSL.Context.set_tls13_ciphersuites`` to set the allowed TLS 1.3 ciphers. +- Added ``OpenSSL.SSL.Connection.set_info_callback`` + +25.1.0 (2025-05-17) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +- Attempting using any methods that mutate an ``OpenSSL.SSL.Context`` after it + has been used to create an ``OpenSSL.SSL.Connection`` will emit a warning. In + a future release, this will raise an exception. + +Changes: +^^^^^^^^ + +* ``cryptography`` maximum version has been increased to 45.0.x. + + +25.0.0 (2025-01-12) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Corrected type annotations on ``Context.set_alpn_select_callback``, ``Context.set_session_cache_mode``, ``Context.set_options``, ``Context.set_mode``, ``X509.subject_name_hash``, and ``X509Store.load_locations``. +- Deprecated APIs are now marked using ``warnings.deprecated``. ``mypy`` will emit deprecation notices for them when used with ``--enable-error-code deprecated``. + +24.3.0 (2024-11-27) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Removed the deprecated ``OpenSSL.crypto.CRL``, ``OpenSSL.crypto.Revoked``, ``OpenSSL.crypto.dump_crl``, and ``OpenSSL.crypto.load_crl``. ``cryptography.x509``'s CRL functionality should be used instead. +- Removed the deprecated ``OpenSSL.crypto.sign`` and ``OpenSSL.crypto.verify``. ``cryptography.hazmat.primitives.asymmetric``'s signature APIs should be used instead. + +Deprecations: +^^^^^^^^^^^^^ + +- Deprecated ``OpenSSL.rand`` - callers should use ``os.urandom()`` instead. +- Deprecated ``add_extensions`` and ``get_extensions`` on ``OpenSSL.crypto.X509Req`` and ``OpenSSL.crypto.X509``. These should have been deprecated at the same time ``X509Extension`` was. Users should use pyca/cryptography's X.509 APIs instead. +- Deprecated ``OpenSSL.crypto.get_elliptic_curves`` and ``OpenSSL.crypto.get_elliptic_curve``, as well as passing the reult of them to ``OpenSSL.SSL.Context.set_tmp_ecdh``, users should instead pass curves from ``cryptography``. +- Deprecated passing ``X509`` objects to ``OpenSSL.SSL.Context.use_certificate``, ``OpenSSL.SSL.Connection.use_certificate``, ``OpenSSL.SSL.Context.add_extra_chain_cert``, and ``OpenSSL.SSL.Context.add_client_ca``, users should instead pass ``cryptography.x509.Certificate`` instances. This is in preparation for deprecating pyOpenSSL's ``X509`` entirely. +- Deprecated passing ``PKey`` objects to ``OpenSSL.SSL.Context.use_privatekey`` and ``OpenSSL.SSL.Connection.use_privatekey``, users should instead pass ``cryptography`` private key instances. This is in preparation for deprecating pyOpenSSL's ``PKey`` entirely. + +Changes: +^^^^^^^^ + +* ``cryptography`` maximum version has been increased to 44.0.x. +* ``OpenSSL.SSL.Connection.get_certificate``, ``OpenSSL.SSL.Connection.get_peer_certificate``, ``OpenSSL.SSL.Connection.get_peer_cert_chain``, and ``OpenSSL.SSL.Connection.get_verified_chain`` now take an ``as_cryptography`` keyword-argument. When ``True`` is passed then ``cryptography.x509.Certificate`` are returned, instead of ``OpenSSL.crypto.X509``. In the future, passing ``False`` (the default) will be deprecated. + + +24.2.1 (2024-07-20) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Fixed changelog to remove sphinx specific restructured text strings. + + +24.2.0 (2024-07-20) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +- Deprecated ``OpenSSL.crypto.X509Req``, ``OpenSSL.crypto.load_certificate_request``, ``OpenSSL.crypto.dump_certificate_request``. Instead, ``cryptography.x509.CertificateSigningRequest``, ``cryptography.x509.CertificateSigningRequestBuilder``, ``cryptography.x509.load_der_x509_csr``, or ``cryptography.x509.load_pem_x509_csr`` should be used. + +Changes: +^^^^^^^^ + +- Added type hints for the ``SSL`` module. + `#1308 `_. +- Changed ``OpenSSL.crypto.PKey.from_cryptography_key`` to accept public and private EC, ED25519, ED448 keys. + `#1310 `_. + +24.1.0 (2024-03-09) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Removed the deprecated ``OpenSSL.crypto.PKCS12`` and + ``OpenSSL.crypto.NetscapeSPKI``. ``OpenSSL.crypto.PKCS12`` may be replaced + by the PKCS#12 APIs in the ``cryptography`` package. + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +24.0.0 (2024-01-22) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Added ``OpenSSL.SSL.Connection.get_selected_srtp_profile`` to determine which SRTP profile was negotiated. + `#1279 `_. + +23.3.0 (2023-10-25) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Dropped support for Python 3.6. +- The minimum ``cryptography`` version is now 41.0.5. +- Removed ``OpenSSL.crypto.load_pkcs7`` and ``OpenSSL.crypto.load_pkcs12`` which had been deprecated for 3 years. +- Added ``OpenSSL.SSL.OP_LEGACY_SERVER_CONNECT`` to allow legacy insecure renegotiation between OpenSSL and unpatched servers. + `#1234 `_. + +Deprecations: +^^^^^^^^^^^^^ + +- Deprecated ``OpenSSL.crypto.PKCS12`` (which was intended to have been deprecated at the same time as ``OpenSSL.crypto.load_pkcs12``). +- Deprecated ``OpenSSL.crypto.NetscapeSPKI``. +- Deprecated ``OpenSSL.crypto.CRL`` +- Deprecated ``OpenSSL.crypto.Revoked`` +- Deprecated ``OpenSSL.crypto.load_crl`` and ``OpenSSL.crypto.dump_crl`` +- Deprecated ``OpenSSL.crypto.sign`` and ``OpenSSL.crypto.verify`` +- Deprecated ``OpenSSL.crypto.X509Extension`` + +Changes: +^^^^^^^^ + +- Changed ``OpenSSL.crypto.X509Store.add_crl`` to also accept + ``cryptography``'s ``x509.CertificateRevocationList`` arguments in addition + to the now deprecated ``OpenSSL.crypto.CRL`` arguments. +- Fixed ``test_set_default_verify_paths`` test so that it is skipped if no + network connection is available. + +23.2.0 (2023-05-30) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Removed ``X509StoreFlags.NOTIFY_POLICY``. + `#1213 `_. + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- ``cryptography`` maximum version has been increased to 41.0.x. +- Invalid versions are now rejected in ``OpenSSL.crypto.X509Req.set_version``. +- Added ``X509VerificationCodes`` to ``OpenSSL.SSL``. + `#1202 `_. + +23.1.1 (2023-03-28) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Worked around an issue in OpenSSL 3.1.0 which caused `X509Extension.get_short_name` to raise an exception when no short name was known to OpenSSL. + `#1204 `_. + +23.1.0 (2023-03-24) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- ``cryptography`` maximum version has been increased to 40.0.x. +- Add ``OpenSSL.SSL.Connection.DTLSv1_get_timeout`` and ``OpenSSL.SSL.Connection.DTLSv1_handle_timeout`` + to support DTLS timeouts `#1180 `_. + +23.0.0 (2023-01-01) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Add ``OpenSSL.SSL.X509StoreFlags.PARTIAL_CHAIN`` constant to allow for users + to perform certificate verification on partial certificate chains. + `#1166 `_ +- ``cryptography`` maximum version has been increased to 39.0.x. + +22.1.0 (2022-09-25) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Remove support for SSLv2 and SSLv3. +- The minimum ``cryptography`` version is now 38.0.x (and we now pin releases + against ``cryptography`` major versions to prevent future breakage) +- The ``OpenSSL.crypto.X509StoreContextError`` exception has been refactored, + changing its internal attributes. + `#1133 `_ + +Deprecations: +^^^^^^^^^^^^^ + +- ``OpenSSL.SSL.SSLeay_version`` is deprecated in favor of + ``OpenSSL.SSL.OpenSSL_version``. The constants ``OpenSSL.SSL.SSLEAY_*`` are + deprecated in favor of ``OpenSSL.SSL.OPENSSL_*``. + +Changes: +^^^^^^^^ + +- Add ``OpenSSL.SSL.Connection.set_verify`` and ``OpenSSL.SSL.Connection.get_verify_mode`` + to override the context object's verification flags. + `#1073 `_ +- Add ``OpenSSL.SSL.Connection.use_certificate`` and ``OpenSSL.SSL.Connection.use_privatekey`` + to set a certificate per connection (and not just per context) `#1121 `_. + +22.0.0 (2022-01-29) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Drop support for Python 2.7. + `#1047 `_ +- The minimum ``cryptography`` version is now 35.0. + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Expose wrappers for some `DTLS + `_ + primitives. `#1026 `_ + +21.0.0 (2021-09-28) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- The minimum ``cryptography`` version is now 3.3. +- Drop support for Python 3.5 + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Raise an error when an invalid ALPN value is set. + `#993 `_ +- Added ``OpenSSL.SSL.Context.set_min_proto_version`` and ``OpenSSL.SSL.Context.set_max_proto_version`` + to set the minimum and maximum supported TLS version `#985 `_. +- Updated ``to_cryptography`` and ``from_cryptography`` methods to support an upcoming release of ``cryptography`` without raising deprecation warnings. + `#1030 `_ + +20.0.1 (2020-12-15) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + +- Fixed compatibility with OpenSSL 1.1.0. + +20.0.0 (2020-11-27) +------------------- + + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- The minimum ``cryptography`` version is now 3.2. +- Remove deprecated ``OpenSSL.tsafe`` module. +- Removed deprecated ``OpenSSL.SSL.Context.set_npn_advertise_callback``, ``OpenSSL.SSL.Context.set_npn_select_callback``, and ``OpenSSL.SSL.Connection.get_next_proto_negotiated``. +- Drop support for Python 3.4 +- Drop support for OpenSSL 1.0.1 and 1.0.2 + +Deprecations: +^^^^^^^^^^^^^ + +- Deprecated ``OpenSSL.crypto.load_pkcs7`` and ``OpenSSL.crypto.load_pkcs12``. + +Changes: +^^^^^^^^ + +- Added a new optional ``chain`` parameter to ``OpenSSL.crypto.X509StoreContext()`` + where additional untrusted certificates can be specified to help chain building. + `#948 `_ +- Added ``OpenSSL.crypto.X509Store.load_locations`` to set trusted + certificate file bundles and/or directories for verification. + `#943 `_ +- Added ``Context.set_keylog_callback`` to log key material. + `#910 `_ +- Added ``OpenSSL.SSL.Connection.get_verified_chain`` to retrieve the + verified certificate chain of the peer. + `#894 `_. +- Make verification callback optional in ``Context.set_verify``. + If omitted, OpenSSL's default verification is used. + `#933 `_ +- Fixed a bug that could truncate or cause a zero-length key error due to a + null byte in private key passphrase in ``OpenSSL.crypto.load_privatekey`` + and ``OpenSSL.crypto.dump_privatekey``. + `#947 `_ + +19.1.0 (2019-11-18) +------------------- + + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Removed deprecated ``ContextType``, ``ConnectionType``, ``PKeyType``, ``X509NameType``, ``X509ReqType``, ``X509Type``, ``X509StoreType``, ``CRLType``, ``PKCS7Type``, ``PKCS12Type``, and ``NetscapeSPKIType`` aliases. + Use the classes without the ``Type`` suffix instead. + `#814 `_ +- The minimum ``cryptography`` version is now 2.8 due to issues on macOS with a transitive dependency. + `#875 `_ + +Deprecations: +^^^^^^^^^^^^^ + +- Deprecated ``OpenSSL.SSL.Context.set_npn_advertise_callback``, ``OpenSSL.SSL.Context.set_npn_select_callback``, and ``OpenSSL.SSL.Connection.get_next_proto_negotiated``. + ALPN should be used instead. + `#820 `_ + + +Changes: +^^^^^^^^ + +- Support ``bytearray`` in ``SSL.Connection.send()`` by using cffi's from_buffer. + `#852 `_ +- The ``OpenSSL.SSL.Context.set_alpn_select_callback`` can return a new ``NO_OVERLAPPING_PROTOCOLS`` sentinel value + to allow a TLS handshake to complete without an application protocol. + + +---- + +19.0.0 (2019-01-21) +------------------- + + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- ``X509Store.add_cert`` no longer raises an error if you add a duplicate cert. + `#787 `_ + + +Deprecations: +^^^^^^^^^^^^^ + +*none* + + +Changes: +^^^^^^^^ + +- pyOpenSSL now works with OpenSSL 1.1.1. + `#805 `_ +- pyOpenSSL now handles NUL bytes in ``X509Name.get_components()`` + `#804 `_ + + + +---- + +18.0.0 (2018-05-16) +------------------- + + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- The minimum ``cryptography`` version is now 2.2.1. +- Support for Python 2.6 has been dropped. + + +Deprecations: +^^^^^^^^^^^^^ + +*none* + + +Changes: +^^^^^^^^ + +- Added ``Connection.get_certificate`` to retrieve the local certificate. + `#733 `_ +- ``OpenSSL.SSL.Connection`` now sets ``SSL_MODE_AUTO_RETRY`` by default. + `#753 `_ +- Added ``Context.set_tlsext_use_srtp`` to enable negotiation of SRTP keying material. + `#734 `_ + + +---- + +17.5.0 (2017-11-30) +------------------- + + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- The minimum ``cryptography`` version is now 2.1.4. + + +Deprecations: +^^^^^^^^^^^^^ + +*none* + + +Changes: +^^^^^^^^ + +- Fixed a potential use-after-free in the verify callback and resolved a memory leak when loading PKCS12 files with ``cacerts``. + `#723 `_ +- Added ``Connection.export_keying_material`` for RFC 5705 compatible export of keying material. + `#725 `_ + +---- + + + +17.4.0 (2017-11-21) +------------------- + + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +*none* + + +Deprecations: +^^^^^^^^^^^^^ + +*none* + + +Changes: +^^^^^^^^ + + +- Re-added a subset of the ``OpenSSL.rand`` module. + This subset allows conscientious users to reseed the OpenSSL CSPRNG after fork. + `#708 `_ +- Corrected a use-after-free when reusing an issuer or subject from an ``X509`` object after the underlying object has been mutated. + `#709 `_ + +---- + + +17.3.0 (2017-09-14) +------------------- + + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Dropped support for Python 3.3. + `#677 `_ +- Removed the deprecated ``OpenSSL.rand`` module. + This is being done ahead of our normal deprecation schedule due to its lack of use and the fact that it was becoming a maintenance burden. + ``os.urandom()`` should be used instead. + `#675 `_ + + +Deprecations: +^^^^^^^^^^^^^ + +- Deprecated ``OpenSSL.tsafe``. + `#673 `_ + +Changes: +^^^^^^^^ + +- Fixed a memory leak in ``OpenSSL.crypto.CRL``. + `#690 `_ +- Fixed a memory leak when verifying certificates with ``OpenSSL.crypto.X509StoreContext``. + `#691 `_ + + +---- + + +17.2.0 (2017-07-20) +------------------- + + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +*none* + + +Deprecations: +^^^^^^^^^^^^^ + +- Deprecated ``OpenSSL.rand`` - callers should use ``os.urandom()`` instead. + `#658 `_ + + +Changes: +^^^^^^^^ + +- Fixed a bug causing ``Context.set_default_verify_paths()`` to not work with cryptography ``manylinux1`` wheels on Python 3.x. + `#665 `_ +- Fixed a crash with (EC)DSA signatures in some cases. + `#670 `_ + + +---- + + +17.1.0 (2017-06-30) +------------------- + + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Removed the deprecated ``OpenSSL.rand.egd()`` function. + Applications should prefer ``os.urandom()`` for random number generation. + `#630 `_ +- Removed the deprecated default ``digest`` argument to ``OpenSSL.crypto.CRL.export()``. + Callers must now always pass an explicit ``digest``. + `#652 `_ +- Fixed a bug with ``ASN1_TIME`` casting in ``X509.set_notBefore()``, + ``X509.set_notAfter()``, ``Revoked.set_rev_date()``, ``Revoked.set_nextUpdate()``, + and ``Revoked.set_lastUpdate()``. You must now pass times in the form + ``YYYYMMDDhhmmssZ``. ``YYYYMMDDhhmmss+hhmm`` and ``YYYYMMDDhhmmss-hhmm`` + will no longer work. `#612 `_ + + +Deprecations: +^^^^^^^^^^^^^ + + +- Deprecated the legacy "Type" aliases: ``ContextType``, ``ConnectionType``, ``PKeyType``, ``X509NameType``, ``X509ExtensionType``, ``X509ReqType``, ``X509Type``, ``X509StoreType``, ``CRLType``, ``PKCS7Type``, ``PKCS12Type``, ``NetscapeSPKIType``. + The names without the "Type"-suffix should be used instead. + + +Changes: +^^^^^^^^ + +- Added ``OpenSSL.crypto.X509.from_cryptography()`` and ``OpenSSL.crypto.X509.to_cryptography()`` for converting X.509 certificate to and from pyca/cryptography objects. + `#640 `_ +- Added ``OpenSSL.crypto.X509Req.from_cryptography()``, ``OpenSSL.crypto.X509Req.to_cryptography()``, ``OpenSSL.crypto.CRL.from_cryptography()``, and ``OpenSSL.crypto.CRL.to_cryptography()`` for converting X.509 CSRs and CRLs to and from pyca/cryptography objects. + `#645 `_ +- Added ``OpenSSL.debug`` that allows to get an overview of used library versions (including linked OpenSSL) and other useful runtime information using ``python -m OpenSSL.debug``. + `#620 `_ +- Added a fallback path to ``Context.set_default_verify_paths()`` to accommodate the upcoming release of ``cryptography`` ``manylinux1`` wheels. + `#633 `_ + + +---- + + +17.0.0 (2017-04-20) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +*none* + + +Deprecations: +^^^^^^^^^^^^^ + +*none* + + +Changes: +^^^^^^^^ + +- Added ``OpenSSL.X509Store.set_time()`` to set a custom verification time when verifying certificate chains. + `#567 `_ +- Added a collection of functions for working with OCSP stapling. + None of these functions make it possible to validate OCSP assertions, only to staple them into the handshake and to retrieve the stapled assertion if provided. + Users will need to write their own code to handle OCSP assertions. + We specifically added: ``Context.set_ocsp_server_callback()``, ``Context.set_ocsp_client_callback()``, and ``Connection.request_ocsp()``. + `#580 `_ +- Changed the ``SSL`` module's memory allocation policy to avoid zeroing memory it allocates when unnecessary. + This reduces CPU usage and memory allocation time by an amount proportional to the size of the allocation. + For applications that process a lot of TLS data or that use very lage allocations this can provide considerable performance improvements. + `#578 `_ +- Automatically set ``SSL_CTX_set_ecdh_auto()`` on ``OpenSSL.SSL.Context``. + `#575 `_ +- Fix empty exceptions from ``OpenSSL.crypto.load_privatekey()``. + `#581 `_ + + +---- + + +16.2.0 (2016-10-15) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +*none* + + +Deprecations: +^^^^^^^^^^^^^ + +*none* + + +Changes: +^^^^^^^^ + +- Fixed compatibility errors with OpenSSL 1.1.0. +- Fixed an issue that caused failures with subinterpreters and embedded Pythons. + `#552 `_ + + +---- + + +16.1.0 (2016-08-26) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +*none* + + +Deprecations: +^^^^^^^^^^^^^ + +- Dropped support for OpenSSL 0.9.8. + + +Changes: +^^^^^^^^ + +- Fix memory leak in ``OpenSSL.crypto.dump_privatekey()`` with ``FILETYPE_TEXT``. + `#496 `_ +- Enable use of CRL (and more) in verify context. + `#483 `_ +- ``OpenSSL.crypto.PKey`` can now be constructed from ``cryptography`` objects and also exported as such. + `#439 `_ +- Support newer versions of ``cryptography`` which use opaque structs for OpenSSL 1.1.0 compatibility. + + +---- + + +16.0.0 (2016-03-19) +------------------- + +This is the first release under full stewardship of PyCA. +We have made *many* changes to make local development more pleasing. +The test suite now passes both on Linux and OS X with OpenSSL 0.9.8, 1.0.1, and 1.0.2. +It has been moved to `pytest `_, all CI test runs are part of `tox `_ and the source code has been made fully `flake8 `_ compliant. + +We hope to have lowered the barrier for contributions significantly but are open to hear about any remaining frustrations. + + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Python 3.2 support has been dropped. + It never had significant real world usage and has been dropped by our main dependency ``cryptography``. + Affected users should upgrade to Python 3.3 or later. + + +Deprecations: +^^^^^^^^^^^^^ + +- The support for EGD has been removed. + The only affected function ``OpenSSL.rand.egd()`` now uses ``os.urandom()`` to seed the internal PRNG instead. + Please see `pyca/cryptography#1636 `_ for more background information on this decision. + In accordance with our backward compatibility policy ``OpenSSL.rand.egd()`` will be *removed* no sooner than a year from the release of 16.0.0. + + Please note that you should `use urandom `_ for all your secure random number needs. +- Python 2.6 support has been deprecated. + Our main dependency ``cryptography`` deprecated 2.6 in version 0.9 (2015-05-14) with no time table for actually dropping it. + pyOpenSSL will drop Python 2.6 support once ``cryptography`` does. + + +Changes: +^^^^^^^^ + +- Fixed ``OpenSSL.SSL.Context.set_session_id``, ``OpenSSL.SSL.Connection.renegotiate``, ``OpenSSL.SSL.Connection.renegotiate_pending``, and ``OpenSSL.SSL.Context.load_client_ca``. + They were lacking an implementation since 0.14. + `#422 `_ +- Fixed segmentation fault when using keys larger than 4096-bit to sign data. + `#428 `_ +- Fixed ``AttributeError`` when ``OpenSSL.SSL.Connection.get_app_data()`` was called before setting any app data. + `#304 `_ +- Added ``OpenSSL.crypto.dump_publickey()`` to dump ``OpenSSL.crypto.PKey`` objects that represent public keys, and ``OpenSSL.crypto.load_publickey()`` to load such objects from serialized representations. + `#382 `_ +- Added ``OpenSSL.crypto.dump_crl()`` to dump a certificate revocation list out to a string buffer. + `#368 `_ +- Added ``OpenSSL.SSL.Connection.get_state_string()`` using the OpenSSL binding ``state_string_long``. + `#358 `_ +- Added support for the ``socket.MSG_PEEK`` flag to ``OpenSSL.SSL.Connection.recv()`` and ``OpenSSL.SSL.Connection.recv_into()``. + `#294 `_ +- Added ``OpenSSL.SSL.Connection.get_protocol_version()`` and ``OpenSSL.SSL.Connection.get_protocol_version_name()``. + `#244 `_ +- Switched to ``utf8string`` mask by default. + OpenSSL formerly defaulted to a ``T61String`` if there were UTF-8 characters present. + This was changed to default to ``UTF8String`` in the config around 2005, but the actual code didn't change it until late last year. + This will default us to the setting that actually works. + To revert this you can call ``OpenSSL.crypto._lib.ASN1_STRING_set_default_mask_asc(b"default")``. + `#234 `_ + + +---- + + +Older Changelog Entries +----------------------- + +The changes from before release 16.0.0 are preserved in the `repository `_. diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst new file mode 100644 index 000000000..dd50c332b --- /dev/null +++ b/CODE_OF_CONDUCT.rst @@ -0,0 +1,55 @@ +Contributor Covenant Code of Conduct +==================================== + +Our Pledge +---------- + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +Our Standards +------------- + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +Our Responsibilities +-------------------- + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +Scope +----- + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. +Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. +Representation of a project may be further defined and clarified by project maintainers. + +Enforcement +----------- + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting either the project maintainer Hynek Schlawack at hs@ox.cx or -- e.g. in case of a conflict of interest -- Amber Brown at hawkowl@atleastfornow.net. +All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. +The project team is obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +Attribution +----------- + +This Code of Conduct is adapted from the `Contributor Covenant `_, version 1.4, available at http://contributor-covenant.org/version/1/4. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a4040e4d9..cf25a729f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -2,42 +2,119 @@ Contributing ============ First of all, thank you for your interest in contributing to pyOpenSSL! +This project has no company backing its development therefore we're dependent on help by the community. + Filing bug reports ------------------ Bug reports are very welcome. -Please file them on the Github issue tracker. +Please file them on the `GitHub issue tracker`_. Good bug reports come with extensive descriptions of the error and how to reproduce it. Reporters are strongly encouraged to include an `short, self contained, correct example `_. + Patches ------- -All patches to pyOpenSSL should be submitted in the form of pull requests to the main pyOpenSSL repository, ``pyca/pyopenssl``. +All patches to pyOpenSSL should be submitted in the form of pull requests to the main pyOpenSSL repository, `pyca/pyopenssl`_. These pull requests should satisfy the following properties: -- The branch referenced should be a `feature branch`_ focusing on one particular improvement to pyOpenSSL. - Create different branches and different pull requests for unrelated features or bugfixes. -- The branch referenced should have a distinctive name (in particular, please do not open pull requests for your ``master`` branch). + +Code +^^^^ + +- The pull request should focus on one particular improvement to pyOpenSSL. + Create different pull requests for unrelated features or bugfixes. - Code should follow `PEP 8`_, especially in the "do what code around you does" sense. - One notable way pyOpenSSL code differs, for example, is that there should be three empty lines between module-level elements,and two empty lines between class-level elements. - Methods and functions are named in ``snake_case``. Follow OpenSSL naming for callables whenever possible is preferred. - Pull requests that introduce code must test all new behavior they introduce as well as for previously untested or poorly tested behavior that they touch. - Pull requests are not allowed to break existing tests. -- Pull requests that introduce features or fix bugs should note those changes in the ``ChangeLog`` text file in the root of the repository. - They should also document the changes, both in docstrings and in the documentation in the ``doc/`` directory. + We usually don't comment on pull requests that are breaking the CI because we consider them work in progress. + Please note that not having 100% code coverage for the code you wrote/touched also causes our CI to fail. + + +Documentation +^^^^^^^^^^^^^ + +When introducing new functionality, please remember to write documentation. + +- New functions and methods should have a docstring describing what they do, what parameters they takes, and what they return. They should also come with `type hints`_. + + .. code-block:: python + + def dump_publickey(type: int, pkey: PKey) -> bytes: + """ + Dump a public key to a buffer. + + :param type: The file type (one of :data:`FILETYPE_PEM` or + :data:`FILETYPE_ASN1`). + :param pkey: The PKey to dump. + + :return: The buffer with the dumped key in it. + """ + + + Don't forget to add an ``.. auto(function|class|method)::`` statement to the relevant API document found in ``doc/api/`` to actually add your function to the Sphinx documentation. +- Do *not* use ``:py:`` prefixes when cross-linking (Python is default). + Do *not* use the generic ``:data:`` or ``:obj:``. + Instead use more specific types like ``:class:``, ``:func:`` or ``:meth:`` if applicable. +- Pull requests that introduce features or fix bugs should note those changes in the CHANGELOG.rst_ file. + Please add new entries to the *top* of the *current* Changes section followed by a line linking to the relevant pull request: + + .. code-block:: rst + + - Added ``OpenSSL.crypto.some_func()`` to do something awesome. + [`#1 `_] + + +- Use `semantic newlines`_ in reStructuredText_ files (files ending in ``.rst``). + + +Review +------ Finally, pull requests must be reviewed before merging. This process mirrors the `cryptography code review process`_. Everyone can perform reviews; this is a very valuable way to contribute, and is highly encouraged. -Pull requests are merged by members of the `pyopenssl-committers team `_. -They should, of course, keep all the requirements detailed in this document as well as the pyca/cryptography merge requirements in mind. +Pull requests are merged by `members of PyCA`_. +They should, of course, keep all the requirements detailed in this document as well as the ``pyca/cryptography`` merge requirements in mind. + +The final responsibility for the reviewing of merged code lies with the person merging it. +Since pyOpenSSL is a sensitive project from a security perspective, reviewers are strongly encouraged to take this review and merge process very seriously. + + +Finding Help +------------ + +If you need any help with the contribution process, you'll find us hanging out at ``#cryptography-dev`` on Freenode_ IRC. +You can also ask questions on our `mailing list`_. + +Please note that this project is released with a Contributor `Code of Conduct`_. +By participating in this project you agree to abide by its terms. + + +Security +-------- + +If you feel that you found a security-relevant bug that you would prefer to discuss in private, please send us a GPG_-encrypted e-mail. + +The maintainer can be reached at hs@ox.cx and his GPG key ID is ``0xAE2536227F69F181`` (Fingerprint: ``C2A0 4F86 ACE2 8ADC F817 DBB7 AE25 3622 7F69 F181``). +Feel free to cross-check this information with Keybase_. -The final responsibility for the reviewing of merged code lies with the person merging it; since pyOpenSSL is obviously a sensitive project from a security perspective, so reviewers are strongly encouraged to take this review and merge process very seriously. -.. _PEP 8: http://legacy.python.org/dev/peps/pep-0008/ +.. _GitHub issue tracker: https://github.com/pyca/pyopenssl/issues +.. _GPG: https://en.wikipedia.org/wiki/GNU_Privacy_Guard +.. _Keybase: https://keybase.io/hynek +.. _pyca/pyopenssl: https://github.com/pyca/pyopenssl +.. _PEP 8: https://www.python.org/dev/peps/pep-0008/ +.. _`type hints`: https://docs.python.org/3/library/typing.html .. _cryptography code review process: https://cryptography.io/en/latest/development/reviewing-patches/ -.. _feature branch: http://nvie.com/posts/a-successful-git-branching-model/ +.. _freenode: https://freenode.net +.. _mailing list: https://mail.python.org/mailman/listinfo/cryptography-dev +.. _members of PyCA: https://github.com/orgs/pyca/people +.. _semantic newlines: http://rhodesmill.org/brandon/2012/one-sentence-per-line/ +.. _reStructuredText: http://sphinx-doc.org/rest.html +.. _CHANGELOG.rst: https://github.com/pyca/pyopenssl/blob/main/CHANGELOG.rst +.. _`Code of Conduct`: https://github.com/pyca/pyopenssl/blob/main/CODE_OF_CONDUCT.rst diff --git a/INSTALL b/INSTALL deleted file mode 100644 index 3af722f5a..000000000 --- a/INSTALL +++ /dev/null @@ -1,33 +0,0 @@ -Installation ------------- -pyOpenSSL uses distutils. Use setup.py to install it in the usual way: - - $ python setup.py install --user - -Or use pip: - - $ pip install --user . - -You can, of course, do - - $ python setup.py --help - -or - - $ pip install --help - -to find out more about how to use these tools. - -Documentation -------------- - -The documentation is written in reStructuredText and build using Sphinx. - -To build the text, html, postscript or dvi forms of the documentation, this is -what you do: - - cd doc - # To make the text-only documentation: - make text - # To make the dvi form: - make dvi diff --git a/INSTALL.rst b/INSTALL.rst new file mode 100644 index 000000000..ba734c9bb --- /dev/null +++ b/INSTALL.rst @@ -0,0 +1,36 @@ +Installation +============ + +To install pyOpenSSL:: + + $ pip install pyopenssl + +If you are installing in order to *develop* on pyOpenSSL, move to the root directory of a pyOpenSSL checkout, and run:: + + $ pip install -e .[test] + + +.. warning:: + + As of 0.14, pyOpenSSL is a pure-Python project. + That means that if you encounter *any* kind of compiler errors, pyOpenSSL's bugtracker is the **wrong** place to report them because we *cannot* help you. + + Please take the time to read the errors and report them/ask help from the appropriate project. + The most likely culprit being `cryptography `_ that contains OpenSSL's library bindings. + + +Supported OpenSSL Versions +-------------------------- + +pyOpenSSL supports the same platforms and releases as the upstream cryptography project `does `_. + +You can always find out the versions of pyOpenSSL, cryptography, and the linked OpenSSL by running ``python -m OpenSSL.debug``. + + +Documentation +------------- + +The documentation is written in reStructuredText and built using Sphinx:: + + $ cd doc + $ make html diff --git a/MANIFEST.in b/MANIFEST.in index 8137258a0..1eba7bd4b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ -include LICENSE ChangeLog INSTALL README TODO MANIFEST.in OpenSSL/RATIONALE -recursive-include doc * -recursive-include examples * -recursive-include rpm * -global-exclude *.pyc +include LICENSE MANIFEST.in *.rst noxfile.py .coveragerc src/OpenSSL/py.typed +exclude .readthedocs.yml mypy.ini +recursive-include tests *.py +recursive-include doc * prune doc/_build diff --git a/OpenSSL/RATIONALE b/OpenSSL/RATIONALE deleted file mode 100644 index a0e389c1e..000000000 --- a/OpenSSL/RATIONALE +++ /dev/null @@ -1,61 +0,0 @@ - RATIONALE - -The reason this module exists at all is that the SSL support in the socket -module in the Python 2.1 distribution (which is what we used, of course I -cannot speak for later versions) is severely limited. - - Update this list whenever needed! The communications module isn't -written yet, so we don't know exactly how this'll work! -This is a list of things we need from an OpenSSL module: - + Context objects (in OpenSSL called SSL_CTX) that can be manipulated from - Python modules. They must support a number of operations: - - Loading certificates from file and memory, both the client - certificate and the certificates used for the verification chain. - - Loading private keys from file and memory. - - Setting the verification mode (basically VERIFY_NONE and - VERIFY_PEER). - - Callbacks mechanism for prompting for pass phrases and verifying - certificates. The callbacks have to work under a multi-threaded - environment (see the comment in ssl/context.c). Of course the - callbacks will have to be written in Python! - + The Connection objects (in OpenSSL called SSL) have to support a few - things: - - Renegotiation, this is really important, especially for connections - that are up and running for a long time, since renegotiation - generates new encryption keys. - - Server-side SSL must work! As far as I know this doesn't work in - the SSL support of the socket module as of Python 2.1. - - Wrapping the methods of the underlying transport object is nice, so - you don't have to keep track of more than one object per connection. - This could of course be done a lot better than the way it works now, - so more transport layers than sockets are possible! - + A well-organized error system that mimics OpenSSL's error system is - desireable. Specifically there has to be a way to find out wether the - operation was successful, or if it failed, why it failed, so some sort - of interface to OpenSSL's error queue mechanism is needed. - + Certificate objects (X509) and certificate name objects (X509_NAME) are - needed, especially for verification purposes. Certificates will - probably also be generated by the server which is another reason for - them to exist. The same thing goes for key objects (EVP_PKEY) - + Since this is an OpenSSL module, there has to be an interface to the - OpenSSL PRNG, so it can be seeded in a good way. - -When asking about SSL on the comp.lang.python newsgroup (or on -python-list@python.org) people usually pointed you to the M2Crypto package. -The M2Crypto.SSL module does implement a lot of OpenSSL's functionality but -unfortunately its error handling system does not seem to be finished, -especially for non-blocking I/O. I think that much of the reason for this -is that M2Crypto is developed using SWIG. This makes it awkward to create -functions that e.g. can return both an integer and NULL since (as far as I -know) you basically write C functions and SWIG makes wrapper functions that -parses the Python argument list and calls your C function, and finally -transforms your return value to a Python object. - -Finally, a good book on the topic of SSL (that I read and learned a lot -from) is "SSL and TLS - Designing and Building Secure Systems" (ISBN -0201615983) by Eric Rescorla. A good mailinglist to subscribe to is the -openssl-users@openssl.org list. - -This comment was written July 2001, discussing Python 2.1. Feel free to -modify it as the SSL support in the socket module changes. - diff --git a/OpenSSL/SSL.py b/OpenSSL/SSL.py deleted file mode 100644 index 7b1cbc1b4..000000000 --- a/OpenSSL/SSL.py +++ /dev/null @@ -1,1559 +0,0 @@ -from sys import platform -from functools import wraps, partial -from itertools import count -from weakref import WeakValueDictionary -from errno import errorcode - -from six import text_type as _text_type -from six import integer_types as integer_types - -from OpenSSL._util import ( - ffi as _ffi, - lib as _lib, - exception_from_error_queue as _exception_from_error_queue, - native as _native) - -from OpenSSL.crypto import ( - FILETYPE_PEM, _PassphraseHelper, PKey, X509Name, X509, X509Store) - -_unspecified = object() - -try: - _memoryview = memoryview -except NameError: - class _memoryview(object): - pass - -try: - _buffer = buffer -except NameError: - class _buffer(object): - pass - -OPENSSL_VERSION_NUMBER = _lib.OPENSSL_VERSION_NUMBER -SSLEAY_VERSION = _lib.SSLEAY_VERSION -SSLEAY_CFLAGS = _lib.SSLEAY_CFLAGS -SSLEAY_PLATFORM = _lib.SSLEAY_PLATFORM -SSLEAY_DIR = _lib.SSLEAY_DIR -SSLEAY_BUILT_ON = _lib.SSLEAY_BUILT_ON - -SENT_SHUTDOWN = _lib.SSL_SENT_SHUTDOWN -RECEIVED_SHUTDOWN = _lib.SSL_RECEIVED_SHUTDOWN - -SSLv2_METHOD = 1 -SSLv3_METHOD = 2 -SSLv23_METHOD = 3 -TLSv1_METHOD = 4 -TLSv1_1_METHOD = 5 -TLSv1_2_METHOD = 6 - -OP_NO_SSLv2 = _lib.SSL_OP_NO_SSLv2 -OP_NO_SSLv3 = _lib.SSL_OP_NO_SSLv3 -OP_NO_TLSv1 = _lib.SSL_OP_NO_TLSv1 - -OP_NO_TLSv1_1 = getattr(_lib, "SSL_OP_NO_TLSv1_1", 0) -OP_NO_TLSv1_2 = getattr(_lib, "SSL_OP_NO_TLSv1_2", 0) - -try: - MODE_RELEASE_BUFFERS = _lib.SSL_MODE_RELEASE_BUFFERS -except AttributeError: - pass - -OP_SINGLE_DH_USE = _lib.SSL_OP_SINGLE_DH_USE -OP_EPHEMERAL_RSA = _lib.SSL_OP_EPHEMERAL_RSA -OP_MICROSOFT_SESS_ID_BUG = _lib.SSL_OP_MICROSOFT_SESS_ID_BUG -OP_NETSCAPE_CHALLENGE_BUG = _lib.SSL_OP_NETSCAPE_CHALLENGE_BUG -OP_NETSCAPE_REUSE_CIPHER_CHANGE_BUG = _lib.SSL_OP_NETSCAPE_REUSE_CIPHER_CHANGE_BUG -OP_SSLREF2_REUSE_CERT_TYPE_BUG = _lib.SSL_OP_SSLREF2_REUSE_CERT_TYPE_BUG -OP_MICROSOFT_BIG_SSLV3_BUFFER = _lib.SSL_OP_MICROSOFT_BIG_SSLV3_BUFFER -try: - OP_MSIE_SSLV2_RSA_PADDING = _lib.SSL_OP_MSIE_SSLV2_RSA_PADDING -except AttributeError: - pass -OP_SSLEAY_080_CLIENT_DH_BUG = _lib.SSL_OP_SSLEAY_080_CLIENT_DH_BUG -OP_TLS_D5_BUG = _lib.SSL_OP_TLS_D5_BUG -OP_TLS_BLOCK_PADDING_BUG = _lib.SSL_OP_TLS_BLOCK_PADDING_BUG -OP_DONT_INSERT_EMPTY_FRAGMENTS = _lib.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS -OP_CIPHER_SERVER_PREFERENCE = _lib.SSL_OP_CIPHER_SERVER_PREFERENCE -OP_TLS_ROLLBACK_BUG = _lib.SSL_OP_TLS_ROLLBACK_BUG -OP_PKCS1_CHECK_1 = _lib.SSL_OP_PKCS1_CHECK_1 -OP_PKCS1_CHECK_2 = _lib.SSL_OP_PKCS1_CHECK_2 -OP_NETSCAPE_CA_DN_BUG = _lib.SSL_OP_NETSCAPE_CA_DN_BUG -OP_NETSCAPE_DEMO_CIPHER_CHANGE_BUG= _lib.SSL_OP_NETSCAPE_DEMO_CIPHER_CHANGE_BUG -try: - OP_NO_COMPRESSION = _lib.SSL_OP_NO_COMPRESSION -except AttributeError: - pass - -OP_NO_QUERY_MTU = _lib.SSL_OP_NO_QUERY_MTU -OP_COOKIE_EXCHANGE = _lib.SSL_OP_COOKIE_EXCHANGE -try: - OP_NO_TICKET = _lib.SSL_OP_NO_TICKET -except AttributeError: - pass - -OP_ALL = _lib.SSL_OP_ALL - -VERIFY_PEER = _lib.SSL_VERIFY_PEER -VERIFY_FAIL_IF_NO_PEER_CERT = _lib.SSL_VERIFY_FAIL_IF_NO_PEER_CERT -VERIFY_CLIENT_ONCE = _lib.SSL_VERIFY_CLIENT_ONCE -VERIFY_NONE = _lib.SSL_VERIFY_NONE - -SESS_CACHE_OFF = _lib.SSL_SESS_CACHE_OFF -SESS_CACHE_CLIENT = _lib.SSL_SESS_CACHE_CLIENT -SESS_CACHE_SERVER = _lib.SSL_SESS_CACHE_SERVER -SESS_CACHE_BOTH = _lib.SSL_SESS_CACHE_BOTH -SESS_CACHE_NO_AUTO_CLEAR = _lib.SSL_SESS_CACHE_NO_AUTO_CLEAR -SESS_CACHE_NO_INTERNAL_LOOKUP = _lib.SSL_SESS_CACHE_NO_INTERNAL_LOOKUP -SESS_CACHE_NO_INTERNAL_STORE = _lib.SSL_SESS_CACHE_NO_INTERNAL_STORE -SESS_CACHE_NO_INTERNAL = _lib.SSL_SESS_CACHE_NO_INTERNAL - -SSL_ST_CONNECT = _lib.SSL_ST_CONNECT -SSL_ST_ACCEPT = _lib.SSL_ST_ACCEPT -SSL_ST_MASK = _lib.SSL_ST_MASK -SSL_ST_INIT = _lib.SSL_ST_INIT -SSL_ST_BEFORE = _lib.SSL_ST_BEFORE -SSL_ST_OK = _lib.SSL_ST_OK -SSL_ST_RENEGOTIATE = _lib.SSL_ST_RENEGOTIATE - -SSL_CB_LOOP = _lib.SSL_CB_LOOP -SSL_CB_EXIT = _lib.SSL_CB_EXIT -SSL_CB_READ = _lib.SSL_CB_READ -SSL_CB_WRITE = _lib.SSL_CB_WRITE -SSL_CB_ALERT = _lib.SSL_CB_ALERT -SSL_CB_READ_ALERT = _lib.SSL_CB_READ_ALERT -SSL_CB_WRITE_ALERT = _lib.SSL_CB_WRITE_ALERT -SSL_CB_ACCEPT_LOOP = _lib.SSL_CB_ACCEPT_LOOP -SSL_CB_ACCEPT_EXIT = _lib.SSL_CB_ACCEPT_EXIT -SSL_CB_CONNECT_LOOP = _lib.SSL_CB_CONNECT_LOOP -SSL_CB_CONNECT_EXIT = _lib.SSL_CB_CONNECT_EXIT -SSL_CB_HANDSHAKE_START = _lib.SSL_CB_HANDSHAKE_START -SSL_CB_HANDSHAKE_DONE = _lib.SSL_CB_HANDSHAKE_DONE - -class Error(Exception): - """ - An error occurred in an `OpenSSL.SSL` API. - """ - - - -_raise_current_error = partial(_exception_from_error_queue, Error) - - -class WantReadError(Error): - pass - - - -class WantWriteError(Error): - pass - - - -class WantX509LookupError(Error): - pass - - - -class ZeroReturnError(Error): - pass - - - -class SysCallError(Error): - pass - - - -class _VerifyHelper(object): - def __init__(self, callback): - self._problems = [] - - @wraps(callback) - def wrapper(ok, store_ctx): - cert = X509.__new__(X509) - cert._x509 = _lib.X509_STORE_CTX_get_current_cert(store_ctx) - error_number = _lib.X509_STORE_CTX_get_error(store_ctx) - error_depth = _lib.X509_STORE_CTX_get_error_depth(store_ctx) - - index = _lib.SSL_get_ex_data_X509_STORE_CTX_idx() - ssl = _lib.X509_STORE_CTX_get_ex_data(store_ctx, index) - connection = Connection._reverse_mapping[ssl] - - try: - result = callback(connection, cert, error_number, error_depth, ok) - except Exception as e: - self._problems.append(e) - return 0 - else: - if result: - _lib.X509_STORE_CTX_set_error(store_ctx, _lib.X509_V_OK) - return 1 - else: - return 0 - - self.callback = _ffi.callback( - "int (*)(int, X509_STORE_CTX *)", wrapper) - - - def raise_if_problem(self): - if self._problems: - try: - _raise_current_error() - except Error: - pass - raise self._problems.pop(0) - - - -def _asFileDescriptor(obj): - fd = None - if not isinstance(obj, integer_types): - meth = getattr(obj, "fileno", None) - if meth is not None: - obj = meth() - - if isinstance(obj, integer_types): - fd = obj - - if not isinstance(fd, integer_types): - raise TypeError("argument must be an int, or have a fileno() method.") - elif fd < 0: - raise ValueError( - "file descriptor cannot be a negative integer (%i)" % (fd,)) - - return fd - - - -def SSLeay_version(type): - """ - Return a string describing the version of OpenSSL in use. - - :param type: One of the SSLEAY_ constants defined in this module. - """ - return _ffi.string(_lib.SSLeay_version(type)) - - - -class Session(object): - pass - - - -class Context(object): - """ - :py:obj:`OpenSSL.SSL.Context` instances define the parameters for setting up - new SSL connections. - """ - _methods = { - SSLv2_METHOD: "SSLv2_method", - SSLv3_METHOD: "SSLv3_method", - SSLv23_METHOD: "SSLv23_method", - TLSv1_METHOD: "TLSv1_method", - TLSv1_1_METHOD: "TLSv1_1_method", - TLSv1_2_METHOD: "TLSv1_2_method", - } - _methods = dict( - (identifier, getattr(_lib, name)) - for (identifier, name) in _methods.items() - if getattr(_lib, name, None) is not None) - - - def __init__(self, method): - """ - :param method: One of SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, or - TLSv1_METHOD. - """ - if not isinstance(method, integer_types): - raise TypeError("method must be an integer") - - try: - method_func = self._methods[method] - except KeyError: - raise ValueError("No such protocol") - - method_obj = method_func() - if method_obj == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - context = _lib.SSL_CTX_new(method_obj) - if context == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - context = _ffi.gc(context, _lib.SSL_CTX_free) - - self._context = context - self._passphrase_helper = None - self._passphrase_callback = None - self._passphrase_userdata = None - self._verify_helper = None - self._verify_callback = None - self._info_callback = None - self._tlsext_servername_callback = None - self._app_data = None - - # SSL_CTX_set_app_data(self->ctx, self); - # SSL_CTX_set_mode(self->ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | - # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | - # SSL_MODE_AUTO_RETRY); - self.set_mode(_lib.SSL_MODE_ENABLE_PARTIAL_WRITE) - - - def load_verify_locations(self, cafile, capath=None): - """ - Let SSL know where we can find trusted certificates for the certificate - chain - - :param cafile: In which file we can find the certificates - :param capath: In which directory we can find the certificates - :return: None - """ - if cafile is None: - cafile = _ffi.NULL - elif not isinstance(cafile, bytes): - raise TypeError("cafile must be None or a byte string") - - if capath is None: - capath = _ffi.NULL - elif not isinstance(capath, bytes): - raise TypeError("capath must be None or a byte string") - - load_result = _lib.SSL_CTX_load_verify_locations(self._context, cafile, capath) - if not load_result: - _raise_current_error() - - - def _wrap_callback(self, callback): - @wraps(callback) - def wrapper(size, verify, userdata): - return callback(size, verify, self._passphrase_userdata) - return _PassphraseHelper( - FILETYPE_PEM, wrapper, more_args=True, truncate=True) - - - def set_passwd_cb(self, callback, userdata=None): - """ - Set the passphrase callback - - :param callback: The Python callback to use - :param userdata: (optional) A Python object which will be given as - argument to the callback - :return: None - """ - if not callable(callback): - raise TypeError("callback must be callable") - - self._passphrase_helper = self._wrap_callback(callback) - self._passphrase_callback = self._passphrase_helper.callback - _lib.SSL_CTX_set_default_passwd_cb( - self._context, self._passphrase_callback) - self._passphrase_userdata = userdata - - - def set_default_verify_paths(self): - """ - Use the platform-specific CA certificate locations - - :return: None - """ - set_result = _lib.SSL_CTX_set_default_verify_paths(self._context) - if not set_result: - # TODO: This is untested. - _raise_current_error() - - - def use_certificate_chain_file(self, certfile): - """ - Load a certificate chain from a file - - :param certfile: The name of the certificate chain file - :return: None - """ - if isinstance(certfile, _text_type): - # Perhaps sys.getfilesystemencoding() could be better? - certfile = certfile.encode("utf-8") - - if not isinstance(certfile, bytes): - raise TypeError("certfile must be bytes or unicode") - - result = _lib.SSL_CTX_use_certificate_chain_file(self._context, certfile) - if not result: - _raise_current_error() - - - def use_certificate_file(self, certfile, filetype=FILETYPE_PEM): - """ - Load a certificate from a file - - :param certfile: The name of the certificate file - :param filetype: (optional) The encoding of the file, default is PEM - :return: None - """ - if isinstance(certfile, _text_type): - # Perhaps sys.getfilesystemencoding() could be better? - certfile = certfile.encode("utf-8") - if not isinstance(certfile, bytes): - raise TypeError("certfile must be bytes or unicode") - if not isinstance(filetype, integer_types): - raise TypeError("filetype must be an integer") - - use_result = _lib.SSL_CTX_use_certificate_file(self._context, certfile, filetype) - if not use_result: - _raise_current_error() - - - def use_certificate(self, cert): - """ - Load a certificate from a X509 object - - :param cert: The X509 object - :return: None - """ - if not isinstance(cert, X509): - raise TypeError("cert must be an X509 instance") - - use_result = _lib.SSL_CTX_use_certificate(self._context, cert._x509) - if not use_result: - _raise_current_error() - - - def add_extra_chain_cert(self, certobj): - """ - Add certificate to chain - - :param certobj: The X509 certificate object to add to the chain - :return: None - """ - if not isinstance(certobj, X509): - raise TypeError("certobj must be an X509 instance") - - copy = _lib.X509_dup(certobj._x509) - add_result = _lib.SSL_CTX_add_extra_chain_cert(self._context, copy) - if not add_result: - # TODO: This is untested. - _lib.X509_free(copy) - _raise_current_error() - - - def _raise_passphrase_exception(self): - if self._passphrase_helper is None: - _raise_current_error() - exception = self._passphrase_helper.raise_if_problem(Error) - if exception is not None: - raise exception - - - def use_privatekey_file(self, keyfile, filetype=_unspecified): - """ - Load a private key from a file - - :param keyfile: The name of the key file - :param filetype: (optional) The encoding of the file, default is PEM - :return: None - """ - if isinstance(keyfile, _text_type): - # Perhaps sys.getfilesystemencoding() could be better? - keyfile = keyfile.encode("utf-8") - - if not isinstance(keyfile, bytes): - raise TypeError("keyfile must be a byte string") - - if filetype is _unspecified: - filetype = FILETYPE_PEM - elif not isinstance(filetype, integer_types): - raise TypeError("filetype must be an integer") - - use_result = _lib.SSL_CTX_use_PrivateKey_file( - self._context, keyfile, filetype) - if not use_result: - self._raise_passphrase_exception() - - - def use_privatekey(self, pkey): - """ - Load a private key from a PKey object - - :param pkey: The PKey object - :return: None - """ - if not isinstance(pkey, PKey): - raise TypeError("pkey must be a PKey instance") - - use_result = _lib.SSL_CTX_use_PrivateKey(self._context, pkey._pkey) - if not use_result: - self._raise_passphrase_exception() - - - def check_privatekey(self): - """ - Check that the private key and certificate match up - - :return: None (raises an exception if something's wrong) - """ - - def load_client_ca(self, cafile): - """ - Load the trusted certificates that will be sent to the client (basically - telling the client "These are the guys I trust"). Does not actually - imply any of the certificates are trusted; that must be configured - separately. - - :param cafile: The name of the certificates file - :return: None - """ - - def set_session_id(self, buf): - """ - Set the session identifier. This is needed if you want to do session - resumption. - - :param buf: A Python object that can be safely converted to a string - :returns: None - """ - - def set_session_cache_mode(self, mode): - """ - Enable/disable session caching and specify the mode used. - - :param mode: One or more of the SESS_CACHE_* flags (combine using - bitwise or) - :returns: The previously set caching mode. - """ - if not isinstance(mode, integer_types): - raise TypeError("mode must be an integer") - - return _lib.SSL_CTX_set_session_cache_mode(self._context, mode) - - - def get_session_cache_mode(self): - """ - :returns: The currently used cache mode. - """ - return _lib.SSL_CTX_get_session_cache_mode(self._context) - - - def set_verify(self, mode, callback): - """ - Set the verify mode and verify callback - - :param mode: The verify mode, this is either VERIFY_NONE or - VERIFY_PEER combined with possible other flags - :param callback: The Python callback to use - :return: None - - See SSL_CTX_set_verify(3SSL) for further details. - """ - if not isinstance(mode, integer_types): - raise TypeError("mode must be an integer") - - if not callable(callback): - raise TypeError("callback must be callable") - - self._verify_helper = _VerifyHelper(callback) - self._verify_callback = self._verify_helper.callback - _lib.SSL_CTX_set_verify(self._context, mode, self._verify_callback) - - - def set_verify_depth(self, depth): - """ - Set the verify depth - - :param depth: An integer specifying the verify depth - :return: None - """ - if not isinstance(depth, integer_types): - raise TypeError("depth must be an integer") - - _lib.SSL_CTX_set_verify_depth(self._context, depth) - - - def get_verify_mode(self): - """ - Get the verify mode - - :return: The verify mode - """ - return _lib.SSL_CTX_get_verify_mode(self._context) - - - def get_verify_depth(self): - """ - Get the verify depth - - :return: The verify depth - """ - return _lib.SSL_CTX_get_verify_depth(self._context) - - - def load_tmp_dh(self, dhfile): - """ - Load parameters for Ephemeral Diffie-Hellman - - :param dhfile: The file to load EDH parameters from - :return: None - """ - if not isinstance(dhfile, bytes): - raise TypeError("dhfile must be a byte string") - - bio = _lib.BIO_new_file(dhfile, b"r") - if bio == _ffi.NULL: - _raise_current_error() - bio = _ffi.gc(bio, _lib.BIO_free) - - dh = _lib.PEM_read_bio_DHparams(bio, _ffi.NULL, _ffi.NULL, _ffi.NULL) - dh = _ffi.gc(dh, _lib.DH_free) - _lib.SSL_CTX_set_tmp_dh(self._context, dh) - - - def set_tmp_ecdh(self, curve): - """ - Select a curve to use for ECDHE key exchange. - - :param curve: A curve object to use as returned by either - :py:meth:`OpenSSL.crypto.get_elliptic_curve` or - :py:meth:`OpenSSL.crypto.get_elliptic_curves`. - - :return: None - """ - _lib.SSL_CTX_set_tmp_ecdh(self._context, curve._to_EC_KEY()) - - - def set_cipher_list(self, cipher_list): - """ - Change the cipher list - - :param cipher_list: A cipher list, see ciphers(1) - :return: None - """ - if isinstance(cipher_list, _text_type): - cipher_list = cipher_list.encode("ascii") - - if not isinstance(cipher_list, bytes): - raise TypeError("cipher_list must be bytes or unicode") - - result = _lib.SSL_CTX_set_cipher_list(self._context, cipher_list) - if not result: - _raise_current_error() - - - def set_client_ca_list(self, certificate_authorities): - """ - Set the list of preferred client certificate signers for this server context. - - This list of certificate authorities will be sent to the client when the - server requests a client certificate. - - :param certificate_authorities: a sequence of X509Names. - :return: None - """ - name_stack = _lib.sk_X509_NAME_new_null() - if name_stack == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - try: - for ca_name in certificate_authorities: - if not isinstance(ca_name, X509Name): - raise TypeError( - "client CAs must be X509Name objects, not %s objects" % ( - type(ca_name).__name__,)) - copy = _lib.X509_NAME_dup(ca_name._name) - if copy == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - push_result = _lib.sk_X509_NAME_push(name_stack, copy) - if not push_result: - _lib.X509_NAME_free(copy) - _raise_current_error() - except: - _lib.sk_X509_NAME_free(name_stack) - raise - - _lib.SSL_CTX_set_client_CA_list(self._context, name_stack) - - - def add_client_ca(self, certificate_authority): - """ - Add the CA certificate to the list of preferred signers for this context. - - The list of certificate authorities will be sent to the client when the - server requests a client certificate. - - :param certificate_authority: certificate authority's X509 certificate. - :return: None - """ - if not isinstance(certificate_authority, X509): - raise TypeError("certificate_authority must be an X509 instance") - - add_result = _lib.SSL_CTX_add_client_CA( - self._context, certificate_authority._x509) - if not add_result: - # TODO: This is untested. - _raise_current_error() - - - def set_timeout(self, timeout): - """ - Set session timeout - - :param timeout: The timeout in seconds - :return: The previous session timeout - """ - if not isinstance(timeout, integer_types): - raise TypeError("timeout must be an integer") - - return _lib.SSL_CTX_set_timeout(self._context, timeout) - - - def get_timeout(self): - """ - Get the session timeout - - :return: The session timeout - """ - return _lib.SSL_CTX_get_timeout(self._context) - - - def set_info_callback(self, callback): - """ - Set the info callback - - :param callback: The Python callback to use - :return: None - """ - @wraps(callback) - def wrapper(ssl, where, return_code): - callback(Connection._reverse_mapping[ssl], where, return_code) - self._info_callback = _ffi.callback( - "void (*)(const SSL *, int, int)", wrapper) - _lib.SSL_CTX_set_info_callback(self._context, self._info_callback) - - - def get_app_data(self): - """ - Get the application data (supplied via set_app_data()) - - :return: The application data - """ - return self._app_data - - - def set_app_data(self, data): - """ - Set the application data (will be returned from get_app_data()) - - :param data: Any Python object - :return: None - """ - self._app_data = data - - - def get_cert_store(self): - """ - Get the certificate store for the context. - - :return: A X509Store object or None if it does not have one. - """ - store = _lib.SSL_CTX_get_cert_store(self._context) - if store == _ffi.NULL: - # TODO: This is untested. - return None - - pystore = X509Store.__new__(X509Store) - pystore._store = store - return pystore - - - def set_options(self, options): - """ - Add options. Options set before are not cleared! - - :param options: The options to add. - :return: The new option bitmask. - """ - if not isinstance(options, integer_types): - raise TypeError("options must be an integer") - - return _lib.SSL_CTX_set_options(self._context, options) - - - def set_mode(self, mode): - """ - Add modes via bitmask. Modes set before are not cleared! - - :param mode: The mode to add. - :return: The new mode bitmask. - """ - if not isinstance(mode, integer_types): - raise TypeError("mode must be an integer") - - return _lib.SSL_CTX_set_mode(self._context, mode) - - - def set_tlsext_servername_callback(self, callback): - """ - Specify a callback function to be called when clients specify a server name. - - :param callback: The callback function. It will be invoked with one - argument, the Connection instance. - """ - @wraps(callback) - def wrapper(ssl, alert, arg): - callback(Connection._reverse_mapping[ssl]) - return 0 - - self._tlsext_servername_callback = _ffi.callback( - "int (*)(const SSL *, int *, void *)", wrapper) - _lib.SSL_CTX_set_tlsext_servername_callback( - self._context, self._tlsext_servername_callback) - -ContextType = Context - - - -class Connection(object): - """ - """ - _reverse_mapping = WeakValueDictionary() - - def __init__(self, context, socket=None): - """ - Create a new Connection object, using the given OpenSSL.SSL.Context - instance and socket. - - :param context: An SSL Context to use for this connection - :param socket: The socket to use for transport layer - """ - if not isinstance(context, Context): - raise TypeError("context must be a Context instance") - - ssl = _lib.SSL_new(context._context) - self._ssl = _ffi.gc(ssl, _lib.SSL_free) - self._context = context - - self._reverse_mapping[self._ssl] = self - - if socket is None: - self._socket = None - # Don't set up any gc for these, SSL_free will take care of them. - self._into_ssl = _lib.BIO_new(_lib.BIO_s_mem()) - self._from_ssl = _lib.BIO_new(_lib.BIO_s_mem()) - - if self._into_ssl == _ffi.NULL or self._from_ssl == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - _lib.SSL_set_bio(self._ssl, self._into_ssl, self._from_ssl) - else: - self._into_ssl = None - self._from_ssl = None - self._socket = socket - set_result = _lib.SSL_set_fd(self._ssl, _asFileDescriptor(self._socket)) - if not set_result: - # TODO: This is untested. - _raise_current_error() - - - def __getattr__(self, name): - """ - Look up attributes on the wrapped socket object if they are not found on - the Connection object. - """ - return getattr(self._socket, name) - - - def _raise_ssl_error(self, ssl, result): - if self._context._verify_helper is not None: - self._context._verify_helper.raise_if_problem() - - error = _lib.SSL_get_error(ssl, result) - if error == _lib.SSL_ERROR_WANT_READ: - raise WantReadError() - elif error == _lib.SSL_ERROR_WANT_WRITE: - raise WantWriteError() - elif error == _lib.SSL_ERROR_ZERO_RETURN: - raise ZeroReturnError() - elif error == _lib.SSL_ERROR_WANT_X509_LOOKUP: - # TODO: This is untested. - raise WantX509LookupError() - elif error == _lib.SSL_ERROR_SYSCALL: - if _lib.ERR_peek_error() == 0: - if result < 0: - if platform == "win32": - errno = _ffi.getwinerror()[0] - else: - errno = _ffi.errno - raise SysCallError(errno, errorcode[errno]) - else: - raise SysCallError(-1, "Unexpected EOF") - else: - # TODO: This is untested. - _raise_current_error() - elif error == _lib.SSL_ERROR_NONE: - pass - else: - _raise_current_error() - - - def get_context(self): - """ - Get session context - """ - return self._context - - - def set_context(self, context): - """ - Switch this connection to a new session context - - :param context: A :py:class:`Context` instance giving the new session - context to use. - """ - if not isinstance(context, Context): - raise TypeError("context must be a Context instance") - - _lib.SSL_set_SSL_CTX(self._ssl, context._context) - self._context = context - - - def get_servername(self): - """ - Retrieve the servername extension value if provided in the client hello - message, or None if there wasn't one. - - :return: A byte string giving the server name or :py:data:`None`. - """ - name = _lib.SSL_get_servername(self._ssl, _lib.TLSEXT_NAMETYPE_host_name) - if name == _ffi.NULL: - return None - - return _ffi.string(name) - - - def set_tlsext_host_name(self, name): - """ - Set the value of the servername extension to send in the client hello. - - :param name: A byte string giving the name. - """ - if not isinstance(name, bytes): - raise TypeError("name must be a byte string") - elif b"\0" in name: - raise TypeError("name must not contain NUL byte") - - # XXX I guess this can fail sometimes? - _lib.SSL_set_tlsext_host_name(self._ssl, name) - - - def pending(self): - """ - Get the number of bytes that can be safely read from the connection - - :return: The number of bytes available in the receive buffer. - """ - return _lib.SSL_pending(self._ssl) - - - def send(self, buf, flags=0): - """ - Send data on the connection. NOTE: If you get one of the WantRead, - WantWrite or WantX509Lookup exceptions on this, you have to call the - method again with the SAME buffer. - - :param buf: The string, buffer or memoryview to send - :param flags: (optional) Included for compatibility with the socket - API, the value is ignored - :return: The number of bytes written - """ - if isinstance(buf, _memoryview): - buf = buf.tobytes() - if isinstance(buf, _buffer): - buf = str(buf) - if not isinstance(buf, bytes): - raise TypeError("data must be a memoryview, buffer or byte string") - - result = _lib.SSL_write(self._ssl, buf, len(buf)) - self._raise_ssl_error(self._ssl, result) - return result - write = send - - - def sendall(self, buf, flags=0): - """ - Send "all" data on the connection. This calls send() repeatedly until - all data is sent. If an error occurs, it's impossible to tell how much - data has been sent. - - :param buf: The string, buffer or memoryview to send - :param flags: (optional) Included for compatibility with the socket - API, the value is ignored - :return: The number of bytes written - """ - if isinstance(buf, _memoryview): - buf = buf.tobytes() - if isinstance(buf, _buffer): - buf = str(buf) - if not isinstance(buf, bytes): - raise TypeError("buf must be a memoryview, buffer or byte string") - - left_to_send = len(buf) - total_sent = 0 - data = _ffi.new("char[]", buf) - - while left_to_send: - result = _lib.SSL_write(self._ssl, data + total_sent, left_to_send) - self._raise_ssl_error(self._ssl, result) - total_sent += result - left_to_send -= result - - - def recv(self, bufsiz, flags=None): - """ - Receive data on the connection. NOTE: If you get one of the WantRead, - WantWrite or WantX509Lookup exceptions on this, you have to call the - method again with the SAME buffer. - - :param bufsiz: The maximum number of bytes to read - :param flags: (optional) Included for compatibility with the socket - API, the value is ignored - :return: The string read from the Connection - """ - buf = _ffi.new("char[]", bufsiz) - result = _lib.SSL_read(self._ssl, buf, bufsiz) - self._raise_ssl_error(self._ssl, result) - return _ffi.buffer(buf, result)[:] - read = recv - - - def _handle_bio_errors(self, bio, result): - if _lib.BIO_should_retry(bio): - if _lib.BIO_should_read(bio): - raise WantReadError() - elif _lib.BIO_should_write(bio): - # TODO: This is untested. - raise WantWriteError() - elif _lib.BIO_should_io_special(bio): - # TODO: This is untested. I think io_special means the socket - # BIO has a not-yet connected socket. - raise ValueError("BIO_should_io_special") - else: - # TODO: This is untested. - raise ValueError("unknown bio failure") - else: - # TODO: This is untested. - _raise_current_error() - - - def bio_read(self, bufsiz): - """ - When using non-socket connections this function reads the "dirty" data - that would have traveled away on the network. - - :param bufsiz: The maximum number of bytes to read - :return: The string read. - """ - if self._from_ssl is None: - raise TypeError("Connection sock was not None") - - if not isinstance(bufsiz, integer_types): - raise TypeError("bufsiz must be an integer") - - buf = _ffi.new("char[]", bufsiz) - result = _lib.BIO_read(self._from_ssl, buf, bufsiz) - if result <= 0: - self._handle_bio_errors(self._from_ssl, result) - - return _ffi.buffer(buf, result)[:] - - - def bio_write(self, buf): - """ - When using non-socket connections this function sends "dirty" data that - would have traveled in on the network. - - :param buf: The string to put into the memory BIO. - :return: The number of bytes written - """ - if self._into_ssl is None: - raise TypeError("Connection sock was not None") - - if not isinstance(buf, bytes): - raise TypeError("buf must be a byte string") - - result = _lib.BIO_write(self._into_ssl, buf, len(buf)) - if result <= 0: - self._handle_bio_errors(self._into_ssl, result) - return result - - - def renegotiate(self): - """ - Renegotiate the session - - :return: True if the renegotiation can be started, false otherwise - """ - - def do_handshake(self): - """ - Perform an SSL handshake (usually called after renegotiate() or one of - set_*_state()). This can raise the same exceptions as send and recv. - - :return: None. - """ - result = _lib.SSL_do_handshake(self._ssl) - self._raise_ssl_error(self._ssl, result) - - - def renegotiate_pending(self): - """ - Check if there's a renegotiation in progress, it will return false once - a renegotiation is finished. - - :return: Whether there's a renegotiation in progress - """ - - def total_renegotiations(self): - """ - Find out the total number of renegotiations. - - :return: The number of renegotiations. - """ - return _lib.SSL_total_renegotiations(self._ssl) - - - def connect(self, addr): - """ - Connect to remote host and set up client-side SSL - - :param addr: A remote address - :return: What the socket's connect method returns - """ - _lib.SSL_set_connect_state(self._ssl) - return self._socket.connect(addr) - - - def connect_ex(self, addr): - """ - Connect to remote host and set up client-side SSL. Note that if the socket's - connect_ex method doesn't return 0, SSL won't be initialized. - - :param addr: A remove address - :return: What the socket's connect_ex method returns - """ - connect_ex = self._socket.connect_ex - self.set_connect_state() - return connect_ex(addr) - - - def accept(self): - """ - Accept incoming connection and set up SSL on it - - :return: A (conn,addr) pair where conn is a Connection and addr is an - address - """ - client, addr = self._socket.accept() - conn = Connection(self._context, client) - conn.set_accept_state() - return (conn, addr) - - - def bio_shutdown(self): - """ - When using non-socket connections this function signals end of - data on the input for this connection. - - :return: None - """ - if self._from_ssl is None: - raise TypeError("Connection sock was not None") - - _lib.BIO_set_mem_eof_return(self._into_ssl, 0) - - - def shutdown(self): - """ - Send closure alert - - :return: True if the shutdown completed successfully (i.e. both sides - have sent closure alerts), false otherwise (i.e. you have to - wait for a ZeroReturnError on a recv() method call - """ - result = _lib.SSL_shutdown(self._ssl) - if result < 0: - # TODO: This is untested. - _raise_current_error() - elif result > 0: - return True - else: - return False - - - def get_cipher_list(self): - """ - Get the session cipher list - - :return: A list of cipher strings - """ - ciphers = [] - for i in count(): - result = _lib.SSL_get_cipher_list(self._ssl, i) - if result == _ffi.NULL: - break - ciphers.append(_native(_ffi.string(result))) - return ciphers - - - def get_client_ca_list(self): - """ - Get CAs whose certificates are suggested for client authentication. - - :return: If this is a server connection, a list of X509Names representing - the acceptable CAs as set by :py:meth:`OpenSSL.SSL.Context.set_client_ca_list` or - :py:meth:`OpenSSL.SSL.Context.add_client_ca`. If this is a client connection, - the list of such X509Names sent by the server, or an empty list if that - has not yet happened. - """ - ca_names = _lib.SSL_get_client_CA_list(self._ssl) - if ca_names == _ffi.NULL: - # TODO: This is untested. - return [] - - result = [] - for i in range(_lib.sk_X509_NAME_num(ca_names)): - name = _lib.sk_X509_NAME_value(ca_names, i) - copy = _lib.X509_NAME_dup(name) - if copy == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - pyname = X509Name.__new__(X509Name) - pyname._name = _ffi.gc(copy, _lib.X509_NAME_free) - result.append(pyname) - return result - - - def makefile(self): - """ - The makefile() method is not implemented, since there is no dup semantics - for SSL connections - - :raise: NotImplementedError - """ - raise NotImplementedError("Cannot make file object of OpenSSL.SSL.Connection") - - - def get_app_data(self): - """ - Get application data - - :return: The application data - """ - return self._app_data - - - def set_app_data(self, data): - """ - Set application data - - :param data - The application data - :return: None - """ - self._app_data = data - - - def get_shutdown(self): - """ - Get shutdown state - - :return: The shutdown state, a bitvector of SENT_SHUTDOWN, RECEIVED_SHUTDOWN. - """ - return _lib.SSL_get_shutdown(self._ssl) - - - def set_shutdown(self, state): - """ - Set shutdown state - - :param state - bitvector of SENT_SHUTDOWN, RECEIVED_SHUTDOWN. - :return: None - """ - if not isinstance(state, integer_types): - raise TypeError("state must be an integer") - - _lib.SSL_set_shutdown(self._ssl, state) - - - def state_string(self): - """ - Get a verbose state description - - :return: A string representing the state - """ - - def server_random(self): - """ - Get a copy of the server hello nonce. - - :return: A string representing the state - """ - if self._ssl.session == _ffi.NULL: - return None - return _ffi.buffer( - self._ssl.s3.server_random, - _lib.SSL3_RANDOM_SIZE)[:] - - - def client_random(self): - """ - Get a copy of the client hello nonce. - - :return: A string representing the state - """ - if self._ssl.session == _ffi.NULL: - return None - return _ffi.buffer( - self._ssl.s3.client_random, - _lib.SSL3_RANDOM_SIZE)[:] - - - def master_key(self): - """ - Get a copy of the master key. - - :return: A string representing the state - """ - if self._ssl.session == _ffi.NULL: - return None - return _ffi.buffer( - self._ssl.session.master_key, - self._ssl.session.master_key_length)[:] - - - def sock_shutdown(self, *args, **kwargs): - """ - See shutdown(2) - - :return: What the socket's shutdown() method returns - """ - return self._socket.shutdown(*args, **kwargs) - - - def get_peer_certificate(self): - """ - Retrieve the other side's certificate (if any) - - :return: The peer's certificate - """ - cert = _lib.SSL_get_peer_certificate(self._ssl) - if cert != _ffi.NULL: - pycert = X509.__new__(X509) - pycert._x509 = _ffi.gc(cert, _lib.X509_free) - return pycert - return None - - - def get_peer_cert_chain(self): - """ - Retrieve the other side's certificate (if any) - - :return: A list of X509 instances giving the peer's certificate chain, - or None if it does not have one. - """ - cert_stack = _lib.SSL_get_peer_cert_chain(self._ssl) - if cert_stack == _ffi.NULL: - return None - - result = [] - for i in range(_lib.sk_X509_num(cert_stack)): - # TODO could incref instead of dup here - cert = _lib.X509_dup(_lib.sk_X509_value(cert_stack, i)) - pycert = X509.__new__(X509) - pycert._x509 = _ffi.gc(cert, _lib.X509_free) - result.append(pycert) - return result - - - def want_read(self): - """ - Checks if more data has to be read from the transport layer to complete an - operation. - - :return: True iff more data has to be read - """ - return _lib.SSL_want_read(self._ssl) - - - def want_write(self): - """ - Checks if there is data to write to the transport layer to complete an - operation. - - :return: True iff there is data to write - """ - return _lib.SSL_want_write(self._ssl) - - - def set_accept_state(self): - """ - Set the connection to work in server mode. The handshake will be handled - automatically by read/write. - - :return: None - """ - _lib.SSL_set_accept_state(self._ssl) - - - def set_connect_state(self): - """ - Set the connection to work in client mode. The handshake will be handled - automatically by read/write. - - :return: None - """ - _lib.SSL_set_connect_state(self._ssl) - - - def get_session(self): - """ - Returns the Session currently used. - - @return: An instance of :py:class:`OpenSSL.SSL.Session` or :py:obj:`None` if - no session exists. - """ - session = _lib.SSL_get1_session(self._ssl) - if session == _ffi.NULL: - return None - - pysession = Session.__new__(Session) - pysession._session = _ffi.gc(session, _lib.SSL_SESSION_free) - return pysession - - - def set_session(self, session): - """ - Set the session to be used when the TLS/SSL connection is established. - - :param session: A Session instance representing the session to use. - :returns: None - """ - if not isinstance(session, Session): - raise TypeError("session must be a Session instance") - - result = _lib.SSL_set_session(self._ssl, session._session) - if not result: - _raise_current_error() - - - def _get_finished_message(self, function): - """ - Helper to implement :py:meth:`get_finished` and - :py:meth:`get_peer_finished`. - - :param function: Either :py:data:`SSL_get_finished`: or - :py:data:`SSL_get_peer_finished`. - - :return: :py:data:`None` if the desired message has not yet been - received, otherwise the contents of the message. - :rtype: :py:class:`bytes` or :py:class:`NoneType` - """ - # The OpenSSL documentation says nothing about what might happen if the - # count argument given is zero. Specifically, it doesn't say whether - # the output buffer may be NULL in that case or not. Inspection of the - # implementation reveals that it calls memcpy() unconditionally. - # Section 7.1.4, paragraph 1 of the C standard suggests that - # memcpy(NULL, source, 0) is not guaranteed to produce defined (let - # alone desirable) behavior (though it probably does on just about - # every implementation...) - # - # Allocate a tiny buffer to pass in (instead of just passing NULL as - # one might expect) for the initial call so as to be safe against this - # potentially undefined behavior. - empty = _ffi.new("char[]", 0) - size = function(self._ssl, empty, 0) - if size == 0: - # No Finished message so far. - return None - - buf = _ffi.new("char[]", size) - function(self._ssl, buf, size) - return _ffi.buffer(buf, size)[:] - - - def get_finished(self): - """ - Obtain the latest `handshake finished` message sent to the peer. - - :return: The contents of the message or :py:obj:`None` if the TLS - handshake has not yet completed. - :rtype: :py:class:`bytes` or :py:class:`NoneType` - """ - return self._get_finished_message(_lib.SSL_get_finished) - - - def get_peer_finished(self): - """ - Obtain the latest `handshake finished` message received from the peer. - - :return: The contents of the message or :py:obj:`None` if the TLS - handshake has not yet completed. - :rtype: :py:class:`bytes` or :py:class:`NoneType` - """ - return self._get_finished_message(_lib.SSL_get_peer_finished) - - - def get_cipher_name(self): - """ - Obtain the name of the currently used cipher. - - :returns: The name of the currently used cipher or :py:obj:`None` - if no connection has been established. - :rtype: :py:class:`unicode` or :py:class:`NoneType` - """ - cipher = _lib.SSL_get_current_cipher(self._ssl) - if cipher == _ffi.NULL: - return None - else: - name = _ffi.string(_lib.SSL_CIPHER_get_name(cipher)) - return name.decode("utf-8") - - - def get_cipher_bits(self): - """ - Obtain the number of secret bits of the currently used cipher. - - :returns: The number of secret bits of the currently used cipher - or :py:obj:`None` if no connection has been established. - :rtype: :py:class:`int` or :py:class:`NoneType` - """ - cipher = _lib.SSL_get_current_cipher(self._ssl) - if cipher == _ffi.NULL: - return None - else: - return _lib.SSL_CIPHER_get_bits(cipher, _ffi.NULL) - - - def get_cipher_version(self): - """ - Obtain the protocol version of the currently used cipher. - - :returns: The protocol name of the currently used cipher - or :py:obj:`None` if no connection has been established. - :rtype: :py:class:`unicode` or :py:class:`NoneType` - """ - cipher = _lib.SSL_get_current_cipher(self._ssl) - if cipher == _ffi.NULL: - return None - else: - version =_ffi.string(_lib.SSL_CIPHER_get_version(cipher)) - return version.decode("utf-8") - - - -ConnectionType = Connection - -# This is similar to the initialization calls at the end of OpenSSL/crypto.py -# but is exercised mostly by the Context initializer. -_lib.SSL_library_init() diff --git a/OpenSSL/__init__.py b/OpenSSL/__init__.py deleted file mode 100644 index db96e1fbd..000000000 --- a/OpenSSL/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (C) AB Strakt -# See LICENSE for details. - -""" -pyOpenSSL - A simple wrapper around the OpenSSL library -""" - -from OpenSSL import rand, crypto, SSL -from OpenSSL.version import __version__ - -__all__ = [ - 'rand', 'crypto', 'SSL', 'tsafe', '__version__'] diff --git a/OpenSSL/_util.py b/OpenSSL/_util.py deleted file mode 100644 index baeecc632..000000000 --- a/OpenSSL/_util.py +++ /dev/null @@ -1,53 +0,0 @@ -from six import PY3, binary_type, text_type - -from cryptography.hazmat.bindings.openssl.binding import Binding -binding = Binding() -ffi = binding.ffi -lib = binding.lib - -def exception_from_error_queue(exceptionType): - def text(charp): - return native(ffi.string(charp)) - - errors = [] - while True: - error = lib.ERR_get_error() - if error == 0: - break - errors.append(( - text(lib.ERR_lib_error_string(error)), - text(lib.ERR_func_error_string(error)), - text(lib.ERR_reason_error_string(error)))) - - raise exceptionType(errors) - - - -def native(s): - """ - Convert :py:class:`bytes` or :py:class:`unicode` to the native - :py:class:`str` type, using UTF-8 encoding if conversion is necessary. - - :raise UnicodeError: The input string is not UTF-8 decodeable. - - :raise TypeError: The input is neither :py:class:`bytes` nor - :py:class:`unicode`. - """ - if not isinstance(s, (binary_type, text_type)): - raise TypeError("%r is neither bytes nor unicode" % s) - if PY3: - if isinstance(s, binary_type): - return s.decode("utf-8") - else: - if isinstance(s, text_type): - return s.encode("utf-8") - return s - - - -if PY3: - def byte_string(s): - return s.encode("charmap") -else: - def byte_string(s): - return s diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py deleted file mode 100644 index 54569eaab..000000000 --- a/OpenSSL/crypto.py +++ /dev/null @@ -1,2489 +0,0 @@ -from time import time -from base64 import b16encode -from functools import partial -from operator import __eq__, __ne__, __lt__, __le__, __gt__, __ge__ - -from six import ( - integer_types as _integer_types, - text_type as _text_type, - PY3 as _PY3) - -from OpenSSL._util import ( - ffi as _ffi, - lib as _lib, - exception_from_error_queue as _exception_from_error_queue, - byte_string as _byte_string, - native as _native) - -FILETYPE_PEM = _lib.SSL_FILETYPE_PEM -FILETYPE_ASN1 = _lib.SSL_FILETYPE_ASN1 - -# TODO This was an API mistake. OpenSSL has no such constant. -FILETYPE_TEXT = 2 ** 16 - 1 - -TYPE_RSA = _lib.EVP_PKEY_RSA -TYPE_DSA = _lib.EVP_PKEY_DSA - - -class Error(Exception): - """ - An error occurred in an `OpenSSL.crypto` API. - """ - - -_raise_current_error = partial(_exception_from_error_queue, Error) - -def _untested_error(where): - """ - An OpenSSL API failed somehow. Additionally, the failure which was - encountered isn't one that's exercised by the test suite so future behavior - of pyOpenSSL is now somewhat less predictable. - """ - raise RuntimeError("Unknown %s failure" % (where,)) - - - -def _new_mem_buf(buffer=None): - """ - Allocate a new OpenSSL memory BIO. - - Arrange for the garbage collector to clean it up automatically. - - :param buffer: None or some bytes to use to put into the BIO so that they - can be read out. - """ - if buffer is None: - bio = _lib.BIO_new(_lib.BIO_s_mem()) - free = _lib.BIO_free - else: - data = _ffi.new("char[]", buffer) - bio = _lib.BIO_new_mem_buf(data, len(buffer)) - # Keep the memory alive as long as the bio is alive! - def free(bio, ref=data): - return _lib.BIO_free(bio) - - if bio == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - bio = _ffi.gc(bio, free) - return bio - - - -def _bio_to_string(bio): - """ - Copy the contents of an OpenSSL BIO object into a Python byte string. - """ - result_buffer = _ffi.new('char**') - buffer_length = _lib.BIO_get_mem_data(bio, result_buffer) - return _ffi.buffer(result_buffer[0], buffer_length)[:] - - - -def _set_asn1_time(boundary, when): - """ - The the time value of an ASN1 time object. - - @param boundary: An ASN1_GENERALIZEDTIME pointer (or an object safely - castable to that type) which will have its value set. - @param when: A string representation of the desired time value. - - @raise TypeError: If C{when} is not a L{bytes} string. - @raise ValueError: If C{when} does not represent a time in the required - format. - @raise RuntimeError: If the time value cannot be set for some other - (unspecified) reason. - """ - if not isinstance(when, bytes): - raise TypeError("when must be a byte string") - - set_result = _lib.ASN1_GENERALIZEDTIME_set_string( - _ffi.cast('ASN1_GENERALIZEDTIME*', boundary), when) - if set_result == 0: - dummy = _ffi.gc(_lib.ASN1_STRING_new(), _lib.ASN1_STRING_free) - _lib.ASN1_STRING_set(dummy, when, len(when)) - check_result = _lib.ASN1_GENERALIZEDTIME_check( - _ffi.cast('ASN1_GENERALIZEDTIME*', dummy)) - if not check_result: - raise ValueError("Invalid string") - else: - _untested_error() - - - -def _get_asn1_time(timestamp): - """ - Retrieve the time value of an ASN1 time object. - - @param timestamp: An ASN1_GENERALIZEDTIME* (or an object safely castable to - that type) from which the time value will be retrieved. - - @return: The time value from C{timestamp} as a L{bytes} string in a certain - format. Or C{None} if the object contains no time value. - """ - string_timestamp = _ffi.cast('ASN1_STRING*', timestamp) - if _lib.ASN1_STRING_length(string_timestamp) == 0: - return None - elif _lib.ASN1_STRING_type(string_timestamp) == _lib.V_ASN1_GENERALIZEDTIME: - return _ffi.string(_lib.ASN1_STRING_data(string_timestamp)) - else: - generalized_timestamp = _ffi.new("ASN1_GENERALIZEDTIME**") - _lib.ASN1_TIME_to_generalizedtime(timestamp, generalized_timestamp) - if generalized_timestamp[0] == _ffi.NULL: - # This may happen: - # - if timestamp was not an ASN1_TIME - # - if allocating memory for the ASN1_GENERALIZEDTIME failed - # - if a copy of the time data from timestamp cannot be made for - # the newly allocated ASN1_GENERALIZEDTIME - # - # These are difficult to test. cffi enforces the ASN1_TIME type. - # Memory allocation failures are a pain to trigger - # deterministically. - _untested_error("ASN1_TIME_to_generalizedtime") - else: - string_timestamp = _ffi.cast( - "ASN1_STRING*", generalized_timestamp[0]) - string_data = _lib.ASN1_STRING_data(string_timestamp) - string_result = _ffi.string(string_data) - _lib.ASN1_GENERALIZEDTIME_free(generalized_timestamp[0]) - return string_result - - - -class PKey(object): - _only_public = False - _initialized = True - - def __init__(self): - pkey = _lib.EVP_PKEY_new() - self._pkey = _ffi.gc(pkey, _lib.EVP_PKEY_free) - self._initialized = False - - - def generate_key(self, type, bits): - """ - Generate a key of a given type, with a given number of a bits - - :param type: The key type (TYPE_RSA or TYPE_DSA) - :param bits: The number of bits - - :return: None - """ - if not isinstance(type, int): - raise TypeError("type must be an integer") - - if not isinstance(bits, int): - raise TypeError("bits must be an integer") - - # TODO Check error return - exponent = _lib.BN_new() - exponent = _ffi.gc(exponent, _lib.BN_free) - _lib.BN_set_word(exponent, _lib.RSA_F4) - - if type == TYPE_RSA: - if bits <= 0: - raise ValueError("Invalid number of bits") - - rsa = _lib.RSA_new() - - result = _lib.RSA_generate_key_ex(rsa, bits, exponent, _ffi.NULL) - if result == 0: - # TODO: The test for this case is commented out. Different - # builds of OpenSSL appear to have different failure modes that - # make it hard to test. Visual inspection of the OpenSSL - # source reveals that a return value of 0 signals an error. - # Manual testing on a particular build of OpenSSL suggests that - # this is probably the appropriate way to handle those errors. - _raise_current_error() - - result = _lib.EVP_PKEY_assign_RSA(self._pkey, rsa) - if not result: - # TODO: It appears as though this can fail if an engine is in - # use which does not support RSA. - _raise_current_error() - - elif type == TYPE_DSA: - dsa = _lib.DSA_generate_parameters( - bits, _ffi.NULL, 0, _ffi.NULL, _ffi.NULL, _ffi.NULL, _ffi.NULL) - if dsa == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - if not _lib.DSA_generate_key(dsa): - # TODO: This is untested. - _raise_current_error() - if not _lib.EVP_PKEY_assign_DSA(self._pkey, dsa): - # TODO: This is untested. - _raise_current_error() - else: - raise Error("No such key type") - - self._initialized = True - - - def check(self): - """ - Check the consistency of an RSA private key. - - :return: True if key is consistent. - :raise Error: if the key is inconsistent. - :raise TypeError: if the key is of a type which cannot be checked. - Only RSA keys can currently be checked. - """ - if self._only_public: - raise TypeError("public key only") - - if _lib.EVP_PKEY_type(self._pkey.type) != _lib.EVP_PKEY_RSA: - raise TypeError("key type unsupported") - - rsa = _lib.EVP_PKEY_get1_RSA(self._pkey) - rsa = _ffi.gc(rsa, _lib.RSA_free) - result = _lib.RSA_check_key(rsa) - if result: - return True - _raise_current_error() - - - def type(self): - """ - Returns the type of the key - - :return: The type of the key. - """ - return self._pkey.type - - - def bits(self): - """ - Returns the number of bits of the key - - :return: The number of bits of the key. - """ - return _lib.EVP_PKEY_bits(self._pkey) -PKeyType = PKey - - - -class _EllipticCurve(object): - """ - A representation of a supported elliptic curve. - - @cvar _curves: :py:obj:`None` until an attempt is made to load the curves. - Thereafter, a :py:type:`set` containing :py:type:`_EllipticCurve` - instances each of which represents one curve supported by the system. - @type _curves: :py:type:`NoneType` or :py:type:`set` - """ - _curves = None - - if _PY3: - # This only necessary on Python 3. Morever, it is broken on Python 2. - def __ne__(self, other): - """ - Implement cooperation with the right-hand side argument of ``!=``. - - Python 3 seems to have dropped this cooperation in this very narrow - circumstance. - """ - if isinstance(other, _EllipticCurve): - return super(_EllipticCurve, self).__ne__(other) - return NotImplemented - - - @classmethod - def _load_elliptic_curves(cls, lib): - """ - Get the curves supported by OpenSSL. - - :param lib: The OpenSSL library binding object. - - :return: A :py:type:`set` of ``cls`` instances giving the names of the - elliptic curves the underlying library supports. - """ - if lib.Cryptography_HAS_EC: - num_curves = lib.EC_get_builtin_curves(_ffi.NULL, 0) - builtin_curves = _ffi.new('EC_builtin_curve[]', num_curves) - # The return value on this call should be num_curves again. We could - # check it to make sure but if it *isn't* then.. what could we do? - # Abort the whole process, I suppose...? -exarkun - lib.EC_get_builtin_curves(builtin_curves, num_curves) - return set( - cls.from_nid(lib, c.nid) - for c in builtin_curves) - return set() - - - @classmethod - def _get_elliptic_curves(cls, lib): - """ - Get, cache, and return the curves supported by OpenSSL. - - :param lib: The OpenSSL library binding object. - - :return: A :py:type:`set` of ``cls`` instances giving the names of the - elliptic curves the underlying library supports. - """ - if cls._curves is None: - cls._curves = cls._load_elliptic_curves(lib) - return cls._curves - - - @classmethod - def from_nid(cls, lib, nid): - """ - Instantiate a new :py:class:`_EllipticCurve` associated with the given - OpenSSL NID. - - :param lib: The OpenSSL library binding object. - - :param nid: The OpenSSL NID the resulting curve object will represent. - This must be a curve NID (and not, for example, a hash NID) or - subsequent operations will fail in unpredictable ways. - :type nid: :py:class:`int` - - :return: The curve object. - """ - return cls(lib, nid, _ffi.string(lib.OBJ_nid2sn(nid)).decode("ascii")) - - - def __init__(self, lib, nid, name): - """ - :param _lib: The :py:mod:`cryptography` binding instance used to - interface with OpenSSL. - - :param _nid: The OpenSSL NID identifying the curve this object - represents. - :type _nid: :py:class:`int` - - :param name: The OpenSSL short name identifying the curve this object - represents. - :type name: :py:class:`unicode` - """ - self._lib = lib - self._nid = nid - self.name = name - - - def __repr__(self): - return "" % (self.name,) - - - def _to_EC_KEY(self): - """ - Create a new OpenSSL EC_KEY structure initialized to use this curve. - - The structure is automatically garbage collected when the Python object - is garbage collected. - """ - key = self._lib.EC_KEY_new_by_curve_name(self._nid) - return _ffi.gc(key, _lib.EC_KEY_free) - - - -def get_elliptic_curves(): - """ - Return a set of objects representing the elliptic curves supported in the - OpenSSL build in use. - - The curve objects have a :py:class:`unicode` ``name`` attribute by which - they identify themselves. - - The curve objects are useful as values for the argument accepted by - :py:meth:`Context.set_tmp_ecdh` to specify which elliptical curve should be - used for ECDHE key exchange. - """ - return _EllipticCurve._get_elliptic_curves(_lib) - - - -def get_elliptic_curve(name): - """ - Return a single curve object selected by name. - - See :py:func:`get_elliptic_curves` for information about curve objects. - - :param name: The OpenSSL short name identifying the curve object to - retrieve. - :type name: :py:class:`unicode` - - If the named curve is not supported then :py:class:`ValueError` is raised. - """ - for curve in get_elliptic_curves(): - if curve.name == name: - return curve - raise ValueError("unknown curve name", name) - - - -class X509Name(object): - def __init__(self, name): - """ - Create a new X509Name, copying the given X509Name instance. - - :param name: An X509Name object to copy - """ - name = _lib.X509_NAME_dup(name._name) - self._name = _ffi.gc(name, _lib.X509_NAME_free) - - - def __setattr__(self, name, value): - if name.startswith('_'): - return super(X509Name, self).__setattr__(name, value) - - # Note: we really do not want str subclasses here, so we do not use - # isinstance. - if type(name) is not str: - raise TypeError("attribute name must be string, not '%.200s'" % ( - type(value).__name__,)) - - nid = _lib.OBJ_txt2nid(_byte_string(name)) - if nid == _lib.NID_undef: - try: - _raise_current_error() - except Error: - pass - raise AttributeError("No such attribute") - - # If there's an old entry for this NID, remove it - for i in range(_lib.X509_NAME_entry_count(self._name)): - ent = _lib.X509_NAME_get_entry(self._name, i) - ent_obj = _lib.X509_NAME_ENTRY_get_object(ent) - ent_nid = _lib.OBJ_obj2nid(ent_obj) - if nid == ent_nid: - ent = _lib.X509_NAME_delete_entry(self._name, i) - _lib.X509_NAME_ENTRY_free(ent) - break - - if isinstance(value, _text_type): - value = value.encode('utf-8') - - add_result = _lib.X509_NAME_add_entry_by_NID( - self._name, nid, _lib.MBSTRING_UTF8, value, -1, -1, 0) - if not add_result: - _raise_current_error() - - - def __getattr__(self, name): - """ - Find attribute. An X509Name object has the following attributes: - countryName (alias C), stateOrProvince (alias ST), locality (alias L), - organization (alias O), organizationalUnit (alias OU), commonName (alias - CN) and more... - """ - nid = _lib.OBJ_txt2nid(_byte_string(name)) - if nid == _lib.NID_undef: - # This is a bit weird. OBJ_txt2nid indicated failure, but it seems - # a lower level function, a2d_ASN1_OBJECT, also feels the need to - # push something onto the error queue. If we don't clean that up - # now, someone else will bump into it later and be quite confused. - # See lp#314814. - try: - _raise_current_error() - except Error: - pass - return super(X509Name, self).__getattr__(name) - - entry_index = _lib.X509_NAME_get_index_by_NID(self._name, nid, -1) - if entry_index == -1: - return None - - entry = _lib.X509_NAME_get_entry(self._name, entry_index) - data = _lib.X509_NAME_ENTRY_get_data(entry) - - result_buffer = _ffi.new("unsigned char**") - data_length = _lib.ASN1_STRING_to_UTF8(result_buffer, data) - if data_length < 0: - # TODO: This is untested. - _raise_current_error() - - try: - result = _ffi.buffer(result_buffer[0], data_length)[:].decode('utf-8') - finally: - # XXX untested - _lib.OPENSSL_free(result_buffer[0]) - return result - - - def _cmp(op): - def f(self, other): - if not isinstance(other, X509Name): - return NotImplemented - result = _lib.X509_NAME_cmp(self._name, other._name) - return op(result, 0) - return f - - __eq__ = _cmp(__eq__) - __ne__ = _cmp(__ne__) - - __lt__ = _cmp(__lt__) - __le__ = _cmp(__le__) - - __gt__ = _cmp(__gt__) - __ge__ = _cmp(__ge__) - - def __repr__(self): - """ - String representation of an X509Name - """ - result_buffer = _ffi.new("char[]", 512); - format_result = _lib.X509_NAME_oneline( - self._name, result_buffer, len(result_buffer)) - - if format_result == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - return "" % ( - _native(_ffi.string(result_buffer)),) - - - def hash(self): - """ - Return the hash value of this name - - :return: None - """ - return _lib.X509_NAME_hash(self._name) - - - def der(self): - """ - Return the DER encoding of this name - - :return: A :py:class:`bytes` instance giving the DER encoded form of - this name. - """ - result_buffer = _ffi.new('unsigned char**') - encode_result = _lib.i2d_X509_NAME(self._name, result_buffer) - if encode_result < 0: - # TODO: This is untested. - _raise_current_error() - - string_result = _ffi.buffer(result_buffer[0], encode_result)[:] - _lib.OPENSSL_free(result_buffer[0]) - return string_result - - - def get_components(self): - """ - Returns the split-up components of this name. - - :return: List of tuples (name, value). - """ - result = [] - for i in range(_lib.X509_NAME_entry_count(self._name)): - ent = _lib.X509_NAME_get_entry(self._name, i) - - fname = _lib.X509_NAME_ENTRY_get_object(ent) - fval = _lib.X509_NAME_ENTRY_get_data(ent) - - nid = _lib.OBJ_obj2nid(fname) - name = _lib.OBJ_nid2sn(nid) - - result.append(( - _ffi.string(name), - _ffi.string( - _lib.ASN1_STRING_data(fval), - _lib.ASN1_STRING_length(fval)))) - - return result -X509NameType = X509Name - - -class X509Extension(object): - def __init__(self, type_name, critical, value, subject=None, issuer=None): - """ - :param typename: The name of the extension to create. - :type typename: :py:data:`str` - - :param critical: A flag indicating whether this is a critical extension. - - :param value: The value of the extension. - :type value: :py:data:`str` - - :param subject: Optional X509 cert to use as subject. - :type subject: :py:class:`X509` - - :param issuer: Optional X509 cert to use as issuer. - :type issuer: :py:class:`X509` - - :return: The X509Extension object - """ - ctx = _ffi.new("X509V3_CTX*") - - # A context is necessary for any extension which uses the r2i conversion - # method. That is, X509V3_EXT_nconf may segfault if passed a NULL ctx. - # Start off by initializing most of the fields to NULL. - _lib.X509V3_set_ctx(ctx, _ffi.NULL, _ffi.NULL, _ffi.NULL, _ffi.NULL, 0) - - # We have no configuration database - but perhaps we should (some - # extensions may require it). - _lib.X509V3_set_ctx_nodb(ctx) - - # Initialize the subject and issuer, if appropriate. ctx is a local, - # and as far as I can tell none of the X509V3_* APIs invoked here steal - # any references, so no need to mess with reference counts or duplicates. - if issuer is not None: - if not isinstance(issuer, X509): - raise TypeError("issuer must be an X509 instance") - ctx.issuer_cert = issuer._x509 - if subject is not None: - if not isinstance(subject, X509): - raise TypeError("subject must be an X509 instance") - ctx.subject_cert = subject._x509 - - if critical: - # There are other OpenSSL APIs which would let us pass in critical - # separately, but they're harder to use, and since value is already - # a pile of crappy junk smuggling a ton of utterly important - # structured data, what's the point of trying to avoid nasty stuff - # with strings? (However, X509V3_EXT_i2d in particular seems like it - # would be a better API to invoke. I do not know where to get the - # ext_struc it desires for its last parameter, though.) - value = b"critical," + value - - extension = _lib.X509V3_EXT_nconf(_ffi.NULL, ctx, type_name, value) - if extension == _ffi.NULL: - _raise_current_error() - self._extension = _ffi.gc(extension, _lib.X509_EXTENSION_free) - - - @property - def _nid(self): - return _lib.OBJ_obj2nid(self._extension.object) - - _prefixes = { - _lib.GEN_EMAIL: "email", - _lib.GEN_DNS: "DNS", - _lib.GEN_URI: "URI", - } - - def _subjectAltNameString(self): - method = _lib.X509V3_EXT_get(self._extension) - if method == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - payload = self._extension.value.data - length = self._extension.value.length - - payloadptr = _ffi.new("unsigned char**") - payloadptr[0] = payload - - if method.it != _ffi.NULL: - ptr = _lib.ASN1_ITEM_ptr(method.it) - data = _lib.ASN1_item_d2i(_ffi.NULL, payloadptr, length, ptr) - names = _ffi.cast("GENERAL_NAMES*", data) - else: - names = _ffi.cast( - "GENERAL_NAMES*", - method.d2i(_ffi.NULL, payloadptr, length)) - - parts = [] - for i in range(_lib.sk_GENERAL_NAME_num(names)): - name = _lib.sk_GENERAL_NAME_value(names, i) - try: - label = self._prefixes[name.type] - except KeyError: - bio = _new_mem_buf() - _lib.GENERAL_NAME_print(bio, name) - parts.append(_native(_bio_to_string(bio))) - else: - value = _native( - _ffi.buffer(name.d.ia5.data, name.d.ia5.length)[:]) - parts.append(label + ":" + value) - return ", ".join(parts) - - - def __str__(self): - """ - :return: a nice text representation of the extension - """ - if _lib.NID_subject_alt_name == self._nid: - return self._subjectAltNameString() - - bio = _new_mem_buf() - print_result = _lib.X509V3_EXT_print(bio, self._extension, 0, 0) - if not print_result: - # TODO: This is untested. - _raise_current_error() - - return _native(_bio_to_string(bio)) - - - def get_critical(self): - """ - Returns the critical field of the X509Extension - - :return: The critical field. - """ - return _lib.X509_EXTENSION_get_critical(self._extension) - - - def get_short_name(self): - """ - Returns the short version of the type name of the X509Extension - - :return: The short type name. - """ - obj = _lib.X509_EXTENSION_get_object(self._extension) - nid = _lib.OBJ_obj2nid(obj) - return _ffi.string(_lib.OBJ_nid2sn(nid)) - - - def get_data(self): - """ - Returns the data of the X509Extension - - :return: A :py:data:`str` giving the X509Extension's ASN.1 encoded data. - """ - octet_result = _lib.X509_EXTENSION_get_data(self._extension) - string_result = _ffi.cast('ASN1_STRING*', octet_result) - char_result = _lib.ASN1_STRING_data(string_result) - result_length = _lib.ASN1_STRING_length(string_result) - return _ffi.buffer(char_result, result_length)[:] - -X509ExtensionType = X509Extension - - -class X509Req(object): - def __init__(self): - req = _lib.X509_REQ_new() - self._req = _ffi.gc(req, _lib.X509_REQ_free) - - - def set_pubkey(self, pkey): - """ - Set the public key of the certificate request - - :param pkey: The public key to use - :return: None - """ - set_result = _lib.X509_REQ_set_pubkey(self._req, pkey._pkey) - if not set_result: - # TODO: This is untested. - _raise_current_error() - - - def get_pubkey(self): - """ - Get the public key from the certificate request - - :return: The public key - """ - pkey = PKey.__new__(PKey) - pkey._pkey = _lib.X509_REQ_get_pubkey(self._req) - if pkey._pkey == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - pkey._pkey = _ffi.gc(pkey._pkey, _lib.EVP_PKEY_free) - pkey._only_public = True - return pkey - - - def set_version(self, version): - """ - Set the version subfield (RFC 2459, section 4.1.2.1) of the certificate - request. - - :param version: The version number - :return: None - """ - set_result = _lib.X509_REQ_set_version(self._req, version) - if not set_result: - _raise_current_error() - - - def get_version(self): - """ - Get the version subfield (RFC 2459, section 4.1.2.1) of the certificate - request. - - :return: an integer giving the value of the version subfield - """ - return _lib.X509_REQ_get_version(self._req) - - - def get_subject(self): - """ - Create an X509Name object for the subject of the certificate request - - :return: An X509Name object - """ - name = X509Name.__new__(X509Name) - name._name = _lib.X509_REQ_get_subject_name(self._req) - if name._name == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - # The name is owned by the X509Req structure. As long as the X509Name - # Python object is alive, keep the X509Req Python object alive. - name._owner = self - - return name - - - def add_extensions(self, extensions): - """ - Add extensions to the request. - - :param extensions: a sequence of X509Extension objects - :return: None - """ - stack = _lib.sk_X509_EXTENSION_new_null() - if stack == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - stack = _ffi.gc(stack, _lib.sk_X509_EXTENSION_free) - - for ext in extensions: - if not isinstance(ext, X509Extension): - raise ValueError("One of the elements is not an X509Extension") - - # TODO push can fail (here and elsewhere) - _lib.sk_X509_EXTENSION_push(stack, ext._extension) - - add_result = _lib.X509_REQ_add_extensions(self._req, stack) - if not add_result: - # TODO: This is untested. - _raise_current_error() - - - def get_extensions(self): - """ - Get extensions to the request. - - :return: A :py:class:`list` of :py:class:`X509Extension` objects. - """ - exts = [] - native_exts_obj = _lib.X509_REQ_get_extensions(self._req) - for i in range(_lib.sk_X509_EXTENSION_num(native_exts_obj)): - ext = X509Extension.__new__(X509Extension) - ext._extension = _lib.sk_X509_EXTENSION_value(native_exts_obj, i) - exts.append(ext) - return exts - - - def sign(self, pkey, digest): - """ - Sign the certificate request using the supplied key and digest - - :param pkey: The key to sign with - :param digest: The message digest to use - :return: None - """ - if pkey._only_public: - raise ValueError("Key has only public part") - - if not pkey._initialized: - raise ValueError("Key is uninitialized") - - digest_obj = _lib.EVP_get_digestbyname(_byte_string(digest)) - if digest_obj == _ffi.NULL: - raise ValueError("No such digest method") - - sign_result = _lib.X509_REQ_sign(self._req, pkey._pkey, digest_obj) - if not sign_result: - # TODO: This is untested. - _raise_current_error() - - - def verify(self, pkey): - """ - Verifies a certificate request using the supplied public key - - :param key: a public key - :return: True if the signature is correct. - - :raise OpenSSL.crypto.Error: If the signature is invalid or there is a - problem verifying the signature. - """ - if not isinstance(pkey, PKey): - raise TypeError("pkey must be a PKey instance") - - result = _lib.X509_REQ_verify(self._req, pkey._pkey) - if result <= 0: - _raise_current_error() - - return result - - -X509ReqType = X509Req - - - -class X509(object): - def __init__(self): - # TODO Allocation failure? And why not __new__ instead of __init__? - x509 = _lib.X509_new() - self._x509 = _ffi.gc(x509, _lib.X509_free) - - - def set_version(self, version): - """ - Set version number of the certificate - - :param version: The version number - :type version: :py:class:`int` - - :return: None - """ - if not isinstance(version, int): - raise TypeError("version must be an integer") - - _lib.X509_set_version(self._x509, version) - - - def get_version(self): - """ - Return version number of the certificate - - :return: Version number as a Python integer - """ - return _lib.X509_get_version(self._x509) - - - def get_pubkey(self): - """ - Get the public key of the certificate - - :return: The public key - """ - pkey = PKey.__new__(PKey) - pkey._pkey = _lib.X509_get_pubkey(self._x509) - if pkey._pkey == _ffi.NULL: - _raise_current_error() - pkey._pkey = _ffi.gc(pkey._pkey, _lib.EVP_PKEY_free) - pkey._only_public = True - return pkey - - - def set_pubkey(self, pkey): - """ - Set the public key of the certificate - - :param pkey: The public key - - :return: None - """ - if not isinstance(pkey, PKey): - raise TypeError("pkey must be a PKey instance") - - set_result = _lib.X509_set_pubkey(self._x509, pkey._pkey) - if not set_result: - _raise_current_error() - - - def sign(self, pkey, digest): - """ - Sign the certificate using the supplied key and digest - - :param pkey: The key to sign with - :param digest: The message digest to use - :return: None - """ - if not isinstance(pkey, PKey): - raise TypeError("pkey must be a PKey instance") - - if pkey._only_public: - raise ValueError("Key only has public part") - - if not pkey._initialized: - raise ValueError("Key is uninitialized") - - evp_md = _lib.EVP_get_digestbyname(_byte_string(digest)) - if evp_md == _ffi.NULL: - raise ValueError("No such digest method") - - sign_result = _lib.X509_sign(self._x509, pkey._pkey, evp_md) - if not sign_result: - _raise_current_error() - - - def get_signature_algorithm(self): - """ - Retrieve the signature algorithm used in the certificate - - :return: A byte string giving the name of the signature algorithm used in - the certificate. - :raise ValueError: If the signature algorithm is undefined. - """ - alg = self._x509.cert_info.signature.algorithm - nid = _lib.OBJ_obj2nid(alg) - if nid == _lib.NID_undef: - raise ValueError("Undefined signature algorithm") - return _ffi.string(_lib.OBJ_nid2ln(nid)) - - - def digest(self, digest_name): - """ - Return the digest of the X509 object. - - :param digest_name: The name of the digest algorithm to use. - :type digest_name: :py:class:`bytes` - - :return: The digest of the object - """ - digest = _lib.EVP_get_digestbyname(_byte_string(digest_name)) - if digest == _ffi.NULL: - raise ValueError("No such digest method") - - result_buffer = _ffi.new("char[]", _lib.EVP_MAX_MD_SIZE) - result_length = _ffi.new("unsigned int[]", 1) - result_length[0] = len(result_buffer) - - digest_result = _lib.X509_digest( - self._x509, digest, result_buffer, result_length) - - if not digest_result: - # TODO: This is untested. - _raise_current_error() - - return b":".join([ - b16encode(ch).upper() for ch - in _ffi.buffer(result_buffer, result_length[0])]) - - - def subject_name_hash(self): - """ - Return the hash of the X509 subject. - - :return: The hash of the subject. - """ - return _lib.X509_subject_name_hash(self._x509) - - - def set_serial_number(self, serial): - """ - Set serial number of the certificate - - :param serial: The serial number - :type serial: :py:class:`int` - - :return: None - """ - if not isinstance(serial, _integer_types): - raise TypeError("serial must be an integer") - - hex_serial = hex(serial)[2:] - if not isinstance(hex_serial, bytes): - hex_serial = hex_serial.encode('ascii') - - bignum_serial = _ffi.new("BIGNUM**") - - # BN_hex2bn stores the result in &bignum. Unless it doesn't feel like - # it. If bignum is still NULL after this call, then the return value is - # actually the result. I hope. -exarkun - small_serial = _lib.BN_hex2bn(bignum_serial, hex_serial) - - if bignum_serial[0] == _ffi.NULL: - set_result = _lib.ASN1_INTEGER_set( - _lib.X509_get_serialNumber(self._x509), small_serial) - if set_result: - # TODO Not tested - _raise_current_error() - else: - asn1_serial = _lib.BN_to_ASN1_INTEGER(bignum_serial[0], _ffi.NULL) - _lib.BN_free(bignum_serial[0]) - if asn1_serial == _ffi.NULL: - # TODO Not tested - _raise_current_error() - asn1_serial = _ffi.gc(asn1_serial, _lib.ASN1_INTEGER_free) - set_result = _lib.X509_set_serialNumber(self._x509, asn1_serial) - if not set_result: - # TODO Not tested - _raise_current_error() - - - def get_serial_number(self): - """ - Return serial number of the certificate - - :return: Serial number as a Python integer - """ - asn1_serial = _lib.X509_get_serialNumber(self._x509) - bignum_serial = _lib.ASN1_INTEGER_to_BN(asn1_serial, _ffi.NULL) - try: - hex_serial = _lib.BN_bn2hex(bignum_serial) - try: - hexstring_serial = _ffi.string(hex_serial) - serial = int(hexstring_serial, 16) - return serial - finally: - _lib.OPENSSL_free(hex_serial) - finally: - _lib.BN_free(bignum_serial) - - - def gmtime_adj_notAfter(self, amount): - """ - Adjust the time stamp for when the certificate stops being valid - - :param amount: The number of seconds by which to adjust the ending - validity time. - :type amount: :py:class:`int` - - :return: None - """ - if not isinstance(amount, int): - raise TypeError("amount must be an integer") - - notAfter = _lib.X509_get_notAfter(self._x509) - _lib.X509_gmtime_adj(notAfter, amount) - - - def gmtime_adj_notBefore(self, amount): - """ - Change the timestamp for when the certificate starts being valid to the current - time plus an offset. - - :param amount: The number of seconds by which to adjust the starting validity - time. - :return: None - """ - if not isinstance(amount, int): - raise TypeError("amount must be an integer") - - notBefore = _lib.X509_get_notBefore(self._x509) - _lib.X509_gmtime_adj(notBefore, amount) - - - def has_expired(self): - """ - Check whether the certificate has expired. - - :return: True if the certificate has expired, false otherwise - """ - now = int(time()) - notAfter = _lib.X509_get_notAfter(self._x509) - return _lib.ASN1_UTCTIME_cmp_time_t( - _ffi.cast('ASN1_UTCTIME*', notAfter), now) < 0 - - - def _get_boundary_time(self, which): - return _get_asn1_time(which(self._x509)) - - - def get_notBefore(self): - """ - Retrieve the time stamp for when the certificate starts being valid - - :return: A string giving the timestamp, in the format:: - - YYYYMMDDhhmmssZ - YYYYMMDDhhmmss+hhmm - YYYYMMDDhhmmss-hhmm - - or None if there is no value set. - """ - return self._get_boundary_time(_lib.X509_get_notBefore) - - - def _set_boundary_time(self, which, when): - return _set_asn1_time(which(self._x509), when) - - - def set_notBefore(self, when): - """ - Set the time stamp for when the certificate starts being valid - - :param when: A string giving the timestamp, in the format: - - YYYYMMDDhhmmssZ - YYYYMMDDhhmmss+hhmm - YYYYMMDDhhmmss-hhmm - :type when: :py:class:`bytes` - - :return: None - """ - return self._set_boundary_time(_lib.X509_get_notBefore, when) - - - def get_notAfter(self): - """ - Retrieve the time stamp for when the certificate stops being valid - - :return: A string giving the timestamp, in the format:: - - YYYYMMDDhhmmssZ - YYYYMMDDhhmmss+hhmm - YYYYMMDDhhmmss-hhmm - - or None if there is no value set. - """ - return self._get_boundary_time(_lib.X509_get_notAfter) - - - def set_notAfter(self, when): - """ - Set the time stamp for when the certificate stops being valid - - :param when: A string giving the timestamp, in the format: - - YYYYMMDDhhmmssZ - YYYYMMDDhhmmss+hhmm - YYYYMMDDhhmmss-hhmm - :type when: :py:class:`bytes` - - :return: None - """ - return self._set_boundary_time(_lib.X509_get_notAfter, when) - - - def _get_name(self, which): - name = X509Name.__new__(X509Name) - name._name = which(self._x509) - if name._name == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - # The name is owned by the X509 structure. As long as the X509Name - # Python object is alive, keep the X509 Python object alive. - name._owner = self - - return name - - - def _set_name(self, which, name): - if not isinstance(name, X509Name): - raise TypeError("name must be an X509Name") - set_result = which(self._x509, name._name) - if not set_result: - # TODO: This is untested. - _raise_current_error() - - - def get_issuer(self): - """ - Create an X509Name object for the issuer of the certificate - - :return: An X509Name object - """ - return self._get_name(_lib.X509_get_issuer_name) - - - def set_issuer(self, issuer): - """ - Set the issuer of the certificate - - :param issuer: The issuer name - :type issuer: :py:class:`X509Name` - - :return: None - """ - return self._set_name(_lib.X509_set_issuer_name, issuer) - - - def get_subject(self): - """ - Create an X509Name object for the subject of the certificate - - :return: An X509Name object - """ - return self._get_name(_lib.X509_get_subject_name) - - - def set_subject(self, subject): - """ - Set the subject of the certificate - - :param subject: The subject name - :type subject: :py:class:`X509Name` - :return: None - """ - return self._set_name(_lib.X509_set_subject_name, subject) - - - def get_extension_count(self): - """ - Get the number of extensions on the certificate. - - :return: The number of extensions as an integer. - """ - return _lib.X509_get_ext_count(self._x509) - - - def add_extensions(self, extensions): - """ - Add extensions to the certificate. - - :param extensions: a sequence of X509Extension objects - :return: None - """ - for ext in extensions: - if not isinstance(ext, X509Extension): - raise ValueError("One of the elements is not an X509Extension") - - add_result = _lib.X509_add_ext(self._x509, ext._extension, -1) - if not add_result: - _raise_current_error() - - - def get_extension(self, index): - """ - Get a specific extension of the certificate by index. - - :param index: The index of the extension to retrieve. - :return: The X509Extension object at the specified index. - """ - ext = X509Extension.__new__(X509Extension) - ext._extension = _lib.X509_get_ext(self._x509, index) - if ext._extension == _ffi.NULL: - raise IndexError("extension index out of bounds") - - extension = _lib.X509_EXTENSION_dup(ext._extension) - ext._extension = _ffi.gc(extension, _lib.X509_EXTENSION_free) - return ext - -X509Type = X509 - - - -class X509Store(object): - def __init__(self): - store = _lib.X509_STORE_new() - self._store = _ffi.gc(store, _lib.X509_STORE_free) - - - def add_cert(self, cert): - if not isinstance(cert, X509): - raise TypeError() - - result = _lib.X509_STORE_add_cert(self._store, cert._x509) - if not result: - _raise_current_error() - - -X509StoreType = X509Store - - - -def load_certificate(type, buffer): - """ - Load a certificate from a buffer - - :param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1) - - :param buffer: The buffer the certificate is stored in - :type buffer: :py:class:`bytes` - - :return: The X509 object - """ - if isinstance(buffer, _text_type): - buffer = buffer.encode("ascii") - - bio = _new_mem_buf(buffer) - - if type == FILETYPE_PEM: - x509 = _lib.PEM_read_bio_X509(bio, _ffi.NULL, _ffi.NULL, _ffi.NULL) - elif type == FILETYPE_ASN1: - x509 = _lib.d2i_X509_bio(bio, _ffi.NULL); - else: - raise ValueError( - "type argument must be FILETYPE_PEM or FILETYPE_ASN1") - - if x509 == _ffi.NULL: - _raise_current_error() - - cert = X509.__new__(X509) - cert._x509 = _ffi.gc(x509, _lib.X509_free) - return cert - - -def dump_certificate(type, cert): - """ - Dump a certificate to a buffer - - :param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1, or - FILETYPE_TEXT) - :param cert: The certificate to dump - :return: The buffer with the dumped certificate in - """ - bio = _new_mem_buf() - - if type == FILETYPE_PEM: - result_code = _lib.PEM_write_bio_X509(bio, cert._x509) - elif type == FILETYPE_ASN1: - result_code = _lib.i2d_X509_bio(bio, cert._x509) - elif type == FILETYPE_TEXT: - result_code = _lib.X509_print_ex(bio, cert._x509, 0, 0) - else: - raise ValueError( - "type argument must be FILETYPE_PEM, FILETYPE_ASN1, or " - "FILETYPE_TEXT") - - return _bio_to_string(bio) - - - -def dump_privatekey(type, pkey, cipher=None, passphrase=None): - """ - Dump a private key to a buffer - - :param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1, or - FILETYPE_TEXT) - :param pkey: The PKey to dump - :param cipher: (optional) if encrypted PEM format, the cipher to - use - :param passphrase: (optional) if encrypted PEM format, this can be either - the passphrase to use, or a callback for providing the - passphrase. - :return: The buffer with the dumped key in - :rtype: :py:data:`str` - """ - bio = _new_mem_buf() - - if cipher is not None: - if passphrase is None: - raise TypeError( - "if a value is given for cipher " - "one must also be given for passphrase") - cipher_obj = _lib.EVP_get_cipherbyname(_byte_string(cipher)) - if cipher_obj == _ffi.NULL: - raise ValueError("Invalid cipher name") - else: - cipher_obj = _ffi.NULL - - helper = _PassphraseHelper(type, passphrase) - if type == FILETYPE_PEM: - result_code = _lib.PEM_write_bio_PrivateKey( - bio, pkey._pkey, cipher_obj, _ffi.NULL, 0, - helper.callback, helper.callback_args) - helper.raise_if_problem() - elif type == FILETYPE_ASN1: - result_code = _lib.i2d_PrivateKey_bio(bio, pkey._pkey) - elif type == FILETYPE_TEXT: - rsa = _lib.EVP_PKEY_get1_RSA(pkey._pkey) - result_code = _lib.RSA_print(bio, rsa, 0) - # TODO RSA_free(rsa)? - else: - raise ValueError( - "type argument must be FILETYPE_PEM, FILETYPE_ASN1, or " - "FILETYPE_TEXT") - - if result_code == 0: - _raise_current_error() - - return _bio_to_string(bio) - - - -def _X509_REVOKED_dup(original): - copy = _lib.X509_REVOKED_new() - if copy == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - if original.serialNumber != _ffi.NULL: - _lib.ASN1_INTEGER_free(copy.serialNumber) - copy.serialNumber = _lib.ASN1_INTEGER_dup(original.serialNumber) - - if original.revocationDate != _ffi.NULL: - _lib.ASN1_TIME_free(copy.revocationDate) - copy.revocationDate = _lib.M_ASN1_TIME_dup(original.revocationDate) - - if original.extensions != _ffi.NULL: - extension_stack = _lib.sk_X509_EXTENSION_new_null() - for i in range(_lib.sk_X509_EXTENSION_num(original.extensions)): - original_ext = _lib.sk_X509_EXTENSION_value(original.extensions, i) - copy_ext = _lib.X509_EXTENSION_dup(original_ext) - _lib.sk_X509_EXTENSION_push(extension_stack, copy_ext) - copy.extensions = extension_stack - - copy.sequence = original.sequence - return copy - - - -class Revoked(object): - # http://www.openssl.org/docs/apps/x509v3_config.html#CRL_distribution_points_ - # which differs from crl_reasons of crypto/x509v3/v3_enum.c that matches - # OCSP_crl_reason_str. We use the latter, just like the command line - # program. - _crl_reasons = [ - b"unspecified", - b"keyCompromise", - b"CACompromise", - b"affiliationChanged", - b"superseded", - b"cessationOfOperation", - b"certificateHold", - # b"removeFromCRL", - ] - - def __init__(self): - revoked = _lib.X509_REVOKED_new() - self._revoked = _ffi.gc(revoked, _lib.X509_REVOKED_free) - - - def set_serial(self, hex_str): - """ - Set the serial number of a revoked Revoked structure - - :param hex_str: The new serial number. - :type hex_str: :py:data:`str` - :return: None - """ - bignum_serial = _ffi.gc(_lib.BN_new(), _lib.BN_free) - bignum_ptr = _ffi.new("BIGNUM**") - bignum_ptr[0] = bignum_serial - bn_result = _lib.BN_hex2bn(bignum_ptr, hex_str) - if not bn_result: - raise ValueError("bad hex string") - - asn1_serial = _ffi.gc( - _lib.BN_to_ASN1_INTEGER(bignum_serial, _ffi.NULL), - _lib.ASN1_INTEGER_free) - _lib.X509_REVOKED_set_serialNumber(self._revoked, asn1_serial) - - - def get_serial(self): - """ - Return the serial number of a Revoked structure - - :return: The serial number as a string - """ - bio = _new_mem_buf() - - result = _lib.i2a_ASN1_INTEGER(bio, self._revoked.serialNumber) - if result < 0: - # TODO: This is untested. - _raise_current_error() - - return _bio_to_string(bio) - - - def _delete_reason(self): - stack = self._revoked.extensions - for i in range(_lib.sk_X509_EXTENSION_num(stack)): - ext = _lib.sk_X509_EXTENSION_value(stack, i) - if _lib.OBJ_obj2nid(ext.object) == _lib.NID_crl_reason: - _lib.X509_EXTENSION_free(ext) - _lib.sk_X509_EXTENSION_delete(stack, i) - break - - - def set_reason(self, reason): - """ - Set the reason of a Revoked object. - - If :py:data:`reason` is :py:data:`None`, delete the reason instead. - - :param reason: The reason string. - :type reason: :py:class:`str` or :py:class:`NoneType` - :return: None - """ - if reason is None: - self._delete_reason() - elif not isinstance(reason, bytes): - raise TypeError("reason must be None or a byte string") - else: - reason = reason.lower().replace(b' ', b'') - reason_code = [r.lower() for r in self._crl_reasons].index(reason) - - new_reason_ext = _lib.ASN1_ENUMERATED_new() - if new_reason_ext == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - new_reason_ext = _ffi.gc(new_reason_ext, _lib.ASN1_ENUMERATED_free) - - set_result = _lib.ASN1_ENUMERATED_set(new_reason_ext, reason_code) - if set_result == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - self._delete_reason() - add_result = _lib.X509_REVOKED_add1_ext_i2d( - self._revoked, _lib.NID_crl_reason, new_reason_ext, 0, 0) - - if not add_result: - # TODO: This is untested. - _raise_current_error() - - - def get_reason(self): - """ - Return the reason of a Revoked object. - - :return: The reason as a string - """ - extensions = self._revoked.extensions - for i in range(_lib.sk_X509_EXTENSION_num(extensions)): - ext = _lib.sk_X509_EXTENSION_value(extensions, i) - if _lib.OBJ_obj2nid(ext.object) == _lib.NID_crl_reason: - bio = _new_mem_buf() - - print_result = _lib.X509V3_EXT_print(bio, ext, 0, 0) - if not print_result: - print_result = _lib.M_ASN1_OCTET_STRING_print(bio, ext.value) - if print_result == 0: - # TODO: This is untested. - _raise_current_error() - - return _bio_to_string(bio) - - - def all_reasons(self): - """ - Return a list of all the supported reason strings. - - :return: A list of reason strings. - """ - return self._crl_reasons[:] - - - def set_rev_date(self, when): - """ - Set the revocation timestamp - - :param when: A string giving the timestamp, in the format: - - YYYYMMDDhhmmssZ - YYYYMMDDhhmmss+hhmm - YYYYMMDDhhmmss-hhmm - - :return: None - """ - return _set_asn1_time(self._revoked.revocationDate, when) - - - def get_rev_date(self): - """ - Retrieve the revocation date - - :return: A string giving the timestamp, in the format: - - YYYYMMDDhhmmssZ - YYYYMMDDhhmmss+hhmm - YYYYMMDDhhmmss-hhmm - """ - return _get_asn1_time(self._revoked.revocationDate) - - - -class CRL(object): - def __init__(self): - """ - Create a new empty CRL object. - """ - crl = _lib.X509_CRL_new() - self._crl = _ffi.gc(crl, _lib.X509_CRL_free) - - - def get_revoked(self): - """ - Return revoked portion of the CRL structure (by value not reference). - - :return: A tuple of Revoked objects. - """ - results = [] - revoked_stack = self._crl.crl.revoked - for i in range(_lib.sk_X509_REVOKED_num(revoked_stack)): - revoked = _lib.sk_X509_REVOKED_value(revoked_stack, i) - revoked_copy = _X509_REVOKED_dup(revoked) - pyrev = Revoked.__new__(Revoked) - pyrev._revoked = _ffi.gc(revoked_copy, _lib.X509_REVOKED_free) - results.append(pyrev) - if results: - return tuple(results) - - - def add_revoked(self, revoked): - """ - Add a revoked (by value not reference) to the CRL structure - - :param revoked: The new revoked. - :type revoked: :class:`X509` - - :return: None - """ - copy = _X509_REVOKED_dup(revoked._revoked) - if copy == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - add_result = _lib.X509_CRL_add0_revoked(self._crl, copy) - if add_result == 0: - # TODO: This is untested. - _raise_current_error() - - - def export(self, cert, key, type=FILETYPE_PEM, days=100): - """ - export a CRL as a string - - :param cert: Used to sign CRL. - :type cert: :class:`X509` - - :param key: Used to sign CRL. - :type key: :class:`PKey` - - :param type: The export format, either :py:data:`FILETYPE_PEM`, :py:data:`FILETYPE_ASN1`, or :py:data:`FILETYPE_TEXT`. - - :param days: The number of days until the next update of this CRL. - :type days: :py:data:`int` - - :return: :py:data:`str` - """ - if not isinstance(cert, X509): - raise TypeError("cert must be an X509 instance") - if not isinstance(key, PKey): - raise TypeError("key must be a PKey instance") - if not isinstance(type, int): - raise TypeError("type must be an integer") - - bio = _lib.BIO_new(_lib.BIO_s_mem()) - if bio == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - # A scratch time object to give different values to different CRL fields - sometime = _lib.ASN1_TIME_new() - if sometime == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - _lib.X509_gmtime_adj(sometime, 0) - _lib.X509_CRL_set_lastUpdate(self._crl, sometime) - - _lib.X509_gmtime_adj(sometime, days * 24 * 60 * 60) - _lib.X509_CRL_set_nextUpdate(self._crl, sometime) - - _lib.X509_CRL_set_issuer_name(self._crl, _lib.X509_get_subject_name(cert._x509)) - - sign_result = _lib.X509_CRL_sign(self._crl, key._pkey, _lib.EVP_md5()) - if not sign_result: - _raise_current_error() - - if type == FILETYPE_PEM: - ret = _lib.PEM_write_bio_X509_CRL(bio, self._crl) - elif type == FILETYPE_ASN1: - ret = _lib.i2d_X509_CRL_bio(bio, self._crl) - elif type == FILETYPE_TEXT: - ret = _lib.X509_CRL_print(bio, self._crl) - else: - raise ValueError( - "type argument must be FILETYPE_PEM, FILETYPE_ASN1, or FILETYPE_TEXT") - - if not ret: - # TODO: This is untested. - _raise_current_error() - - return _bio_to_string(bio) -CRLType = CRL - - - -class PKCS7(object): - def type_is_signed(self): - """ - Check if this NID_pkcs7_signed object - - :return: True if the PKCS7 is of type signed - """ - if _lib.PKCS7_type_is_signed(self._pkcs7): - return True - return False - - - def type_is_enveloped(self): - """ - Check if this NID_pkcs7_enveloped object - - :returns: True if the PKCS7 is of type enveloped - """ - if _lib.PKCS7_type_is_enveloped(self._pkcs7): - return True - return False - - - def type_is_signedAndEnveloped(self): - """ - Check if this NID_pkcs7_signedAndEnveloped object - - :returns: True if the PKCS7 is of type signedAndEnveloped - """ - if _lib.PKCS7_type_is_signedAndEnveloped(self._pkcs7): - return True - return False - - - def type_is_data(self): - """ - Check if this NID_pkcs7_data object - - :return: True if the PKCS7 is of type data - """ - if _lib.PKCS7_type_is_data(self._pkcs7): - return True - return False - - - def get_type_name(self): - """ - Returns the type name of the PKCS7 structure - - :return: A string with the typename - """ - nid = _lib.OBJ_obj2nid(self._pkcs7.type) - string_type = _lib.OBJ_nid2sn(nid) - return _ffi.string(string_type) - -PKCS7Type = PKCS7 - - - -class PKCS12(object): - def __init__(self): - self._pkey = None - self._cert = None - self._cacerts = None - self._friendlyname = None - - - def get_certificate(self): - """ - Return certificate portion of the PKCS12 structure - - :return: X509 object containing the certificate - """ - return self._cert - - - def set_certificate(self, cert): - """ - Replace the certificate portion of the PKCS12 structure - - :param cert: The new certificate. - :type cert: :py:class:`X509` or :py:data:`None` - :return: None - """ - if not isinstance(cert, X509): - raise TypeError("cert must be an X509 instance") - self._cert = cert - - - def get_privatekey(self): - """ - Return private key portion of the PKCS12 structure - - :returns: PKey object containing the private key - """ - return self._pkey - - - def set_privatekey(self, pkey): - """ - Replace or set the certificate portion of the PKCS12 structure - - :param pkey: The new private key. - :type pkey: :py:class:`PKey` - :return: None - """ - if not isinstance(pkey, PKey): - raise TypeError("pkey must be a PKey instance") - self._pkey = pkey - - - def get_ca_certificates(self): - """ - Return CA certificates within of the PKCS12 object - - :return: A newly created tuple containing the CA certificates in the chain, - if any are present, or None if no CA certificates are present. - """ - if self._cacerts is not None: - return tuple(self._cacerts) - - - def set_ca_certificates(self, cacerts): - """ - Replace or set the CA certificates withing the PKCS12 object. - - :param cacerts: The new CA certificates. - :type cacerts: :py:data:`None` or an iterable of :py:class:`X509` - :return: None - """ - if cacerts is None: - self._cacerts = None - else: - cacerts = list(cacerts) - for cert in cacerts: - if not isinstance(cert, X509): - raise TypeError("iterable must only contain X509 instances") - self._cacerts = cacerts - - - def set_friendlyname(self, name): - """ - Replace or set the certificate portion of the PKCS12 structure - - :param name: The new friendly name. - :type name: :py:class:`bytes` - :return: None - """ - if name is None: - self._friendlyname = None - elif not isinstance(name, bytes): - raise TypeError("name must be a byte string or None (not %r)" % (name,)) - self._friendlyname = name - - - def get_friendlyname(self): - """ - Return friendly name portion of the PKCS12 structure - - :returns: String containing the friendlyname - """ - return self._friendlyname - - - def export(self, passphrase=None, iter=2048, maciter=1): - """ - Dump a PKCS12 object as a string. See also "man PKCS12_create". - - :param passphrase: used to encrypt the PKCS12 - :type passphrase: :py:data:`bytes` - - :param iter: How many times to repeat the encryption - :type iter: :py:data:`int` - - :param maciter: How many times to repeat the MAC - :type maciter: :py:data:`int` - - :return: The string containing the PKCS12 - """ - if self._cacerts is None: - cacerts = _ffi.NULL - else: - cacerts = _lib.sk_X509_new_null() - cacerts = _ffi.gc(cacerts, _lib.sk_X509_free) - for cert in self._cacerts: - _lib.sk_X509_push(cacerts, cert._x509) - - if passphrase is None: - passphrase = _ffi.NULL - - friendlyname = self._friendlyname - if friendlyname is None: - friendlyname = _ffi.NULL - - if self._pkey is None: - pkey = _ffi.NULL - else: - pkey = self._pkey._pkey - - if self._cert is None: - cert = _ffi.NULL - else: - cert = self._cert._x509 - - pkcs12 = _lib.PKCS12_create( - passphrase, friendlyname, pkey, cert, cacerts, - _lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC, - _lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC, - iter, maciter, 0) - if pkcs12 == _ffi.NULL: - _raise_current_error() - pkcs12 = _ffi.gc(pkcs12, _lib.PKCS12_free) - - bio = _new_mem_buf() - _lib.i2d_PKCS12_bio(bio, pkcs12) - return _bio_to_string(bio) - -PKCS12Type = PKCS12 - - - -class NetscapeSPKI(object): - def __init__(self): - spki = _lib.NETSCAPE_SPKI_new() - self._spki = _ffi.gc(spki, _lib.NETSCAPE_SPKI_free) - - - def sign(self, pkey, digest): - """ - Sign the certificate request using the supplied key and digest - - :param pkey: The key to sign with - :param digest: The message digest to use - :return: None - """ - if pkey._only_public: - raise ValueError("Key has only public part") - - if not pkey._initialized: - raise ValueError("Key is uninitialized") - - digest_obj = _lib.EVP_get_digestbyname(_byte_string(digest)) - if digest_obj == _ffi.NULL: - raise ValueError("No such digest method") - - sign_result = _lib.NETSCAPE_SPKI_sign(self._spki, pkey._pkey, digest_obj) - if not sign_result: - # TODO: This is untested. - _raise_current_error() - - - def verify(self, key): - """ - Verifies a certificate request using the supplied public key - - :param key: a public key - :return: True if the signature is correct. - :raise OpenSSL.crypto.Error: If the signature is invalid or there is a - problem verifying the signature. - """ - answer = _lib.NETSCAPE_SPKI_verify(self._spki, key._pkey) - if answer <= 0: - _raise_current_error() - return True - - - def b64_encode(self): - """ - Generate a base64 encoded string from an SPKI - - :return: The base64 encoded string - """ - encoded = _lib.NETSCAPE_SPKI_b64_encode(self._spki) - result = _ffi.string(encoded) - _lib.CRYPTO_free(encoded) - return result - - - def get_pubkey(self): - """ - Get the public key of the certificate - - :return: The public key - """ - pkey = PKey.__new__(PKey) - pkey._pkey = _lib.NETSCAPE_SPKI_get_pubkey(self._spki) - if pkey._pkey == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - pkey._pkey = _ffi.gc(pkey._pkey, _lib.EVP_PKEY_free) - pkey._only_public = True - return pkey - - - def set_pubkey(self, pkey): - """ - Set the public key of the certificate - - :param pkey: The public key - :return: None - """ - set_result = _lib.NETSCAPE_SPKI_set_pubkey(self._spki, pkey._pkey) - if not set_result: - # TODO: This is untested. - _raise_current_error() -NetscapeSPKIType = NetscapeSPKI - - -class _PassphraseHelper(object): - def __init__(self, type, passphrase, more_args=False, truncate=False): - if type != FILETYPE_PEM and passphrase is not None: - raise ValueError("only FILETYPE_PEM key format supports encryption") - self._passphrase = passphrase - self._more_args = more_args - self._truncate = truncate - self._problems = [] - - - @property - def callback(self): - if self._passphrase is None: - return _ffi.NULL - elif isinstance(self._passphrase, bytes): - return _ffi.NULL - elif callable(self._passphrase): - return _ffi.callback("pem_password_cb", self._read_passphrase) - else: - raise TypeError("Last argument must be string or callable") - - - @property - def callback_args(self): - if self._passphrase is None: - return _ffi.NULL - elif isinstance(self._passphrase, bytes): - return self._passphrase - elif callable(self._passphrase): - return _ffi.NULL - else: - raise TypeError("Last argument must be string or callable") - - - def raise_if_problem(self, exceptionType=Error): - try: - _exception_from_error_queue(exceptionType) - except exceptionType as e: - from_queue = e - if self._problems: - raise self._problems[0] - return from_queue - - - def _read_passphrase(self, buf, size, rwflag, userdata): - try: - if self._more_args: - result = self._passphrase(size, rwflag, userdata) - else: - result = self._passphrase(rwflag) - if not isinstance(result, bytes): - raise ValueError("String expected") - if len(result) > size: - if self._truncate: - result = result[:size] - else: - raise ValueError("passphrase returned by callback is too long") - for i in range(len(result)): - buf[i] = result[i:i + 1] - return len(result) - except Exception as e: - self._problems.append(e) - return 0 - - - -def load_privatekey(type, buffer, passphrase=None): - """ - Load a private key from a buffer - - :param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1) - :param buffer: The buffer the key is stored in - :param passphrase: (optional) if encrypted PEM format, this can be - either the passphrase to use, or a callback for - providing the passphrase. - - :return: The PKey object - """ - if isinstance(buffer, _text_type): - buffer = buffer.encode("ascii") - - bio = _new_mem_buf(buffer) - - helper = _PassphraseHelper(type, passphrase) - if type == FILETYPE_PEM: - evp_pkey = _lib.PEM_read_bio_PrivateKey( - bio, _ffi.NULL, helper.callback, helper.callback_args) - helper.raise_if_problem() - elif type == FILETYPE_ASN1: - evp_pkey = _lib.d2i_PrivateKey_bio(bio, _ffi.NULL) - else: - raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1") - - if evp_pkey == _ffi.NULL: - _raise_current_error() - - pkey = PKey.__new__(PKey) - pkey._pkey = _ffi.gc(evp_pkey, _lib.EVP_PKEY_free) - return pkey - - - -def dump_certificate_request(type, req): - """ - Dump a certificate request to a buffer - - :param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1) - :param req: The certificate request to dump - :return: The buffer with the dumped certificate request in - """ - bio = _new_mem_buf() - - if type == FILETYPE_PEM: - result_code = _lib.PEM_write_bio_X509_REQ(bio, req._req) - elif type == FILETYPE_ASN1: - result_code = _lib.i2d_X509_REQ_bio(bio, req._req) - elif type == FILETYPE_TEXT: - result_code = _lib.X509_REQ_print_ex(bio, req._req, 0, 0) - else: - raise ValueError("type argument must be FILETYPE_PEM, FILETYPE_ASN1, or FILETYPE_TEXT") - - if result_code == 0: - # TODO: This is untested. - _raise_current_error() - - return _bio_to_string(bio) - - - -def load_certificate_request(type, buffer): - """ - Load a certificate request from a buffer - - :param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1) - :param buffer: The buffer the certificate request is stored in - :return: The X509Req object - """ - if isinstance(buffer, _text_type): - buffer = buffer.encode("ascii") - - bio = _new_mem_buf(buffer) - - if type == FILETYPE_PEM: - req = _lib.PEM_read_bio_X509_REQ(bio, _ffi.NULL, _ffi.NULL, _ffi.NULL) - elif type == FILETYPE_ASN1: - req = _lib.d2i_X509_REQ_bio(bio, _ffi.NULL) - else: - raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1") - - if req == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - - x509req = X509Req.__new__(X509Req) - x509req._req = _ffi.gc(req, _lib.X509_REQ_free) - return x509req - - - -def sign(pkey, data, digest): - """ - Sign data with a digest - - :param pkey: Pkey to sign with - :param data: data to be signed - :param digest: message digest to use - :return: signature - """ - digest_obj = _lib.EVP_get_digestbyname(_byte_string(digest)) - if digest_obj == _ffi.NULL: - raise ValueError("No such digest method") - - md_ctx = _ffi.new("EVP_MD_CTX*") - md_ctx = _ffi.gc(md_ctx, _lib.EVP_MD_CTX_cleanup) - - _lib.EVP_SignInit(md_ctx, digest_obj) - _lib.EVP_SignUpdate(md_ctx, data, len(data)) - - signature_buffer = _ffi.new("unsigned char[]", 512) - signature_length = _ffi.new("unsigned int*") - signature_length[0] = len(signature_buffer) - final_result = _lib.EVP_SignFinal( - md_ctx, signature_buffer, signature_length, pkey._pkey) - - if final_result != 1: - # TODO: This is untested. - _raise_current_error() - - return _ffi.buffer(signature_buffer, signature_length[0])[:] - - - -def verify(cert, signature, data, digest): - """ - Verify a signature - - :param cert: signing certificate (X509 object) - :param signature: signature returned by sign function - :param data: data to be verified - :param digest: message digest to use - :return: None if the signature is correct, raise exception otherwise - """ - digest_obj = _lib.EVP_get_digestbyname(_byte_string(digest)) - if digest_obj == _ffi.NULL: - raise ValueError("No such digest method") - - pkey = _lib.X509_get_pubkey(cert._x509) - if pkey == _ffi.NULL: - # TODO: This is untested. - _raise_current_error() - pkey = _ffi.gc(pkey, _lib.EVP_PKEY_free) - - md_ctx = _ffi.new("EVP_MD_CTX*") - md_ctx = _ffi.gc(md_ctx, _lib.EVP_MD_CTX_cleanup) - - _lib.EVP_VerifyInit(md_ctx, digest_obj) - _lib.EVP_VerifyUpdate(md_ctx, data, len(data)) - verify_result = _lib.EVP_VerifyFinal(md_ctx, signature, len(signature), pkey) - - if verify_result != 1: - _raise_current_error() - - - -def load_crl(type, buffer): - """ - Load a certificate revocation list from a buffer - - :param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1) - :param buffer: The buffer the CRL is stored in - - :return: The PKey object - """ - if isinstance(buffer, _text_type): - buffer = buffer.encode("ascii") - - bio = _new_mem_buf(buffer) - - if type == FILETYPE_PEM: - crl = _lib.PEM_read_bio_X509_CRL(bio, _ffi.NULL, _ffi.NULL, _ffi.NULL) - elif type == FILETYPE_ASN1: - crl = _lib.d2i_X509_CRL_bio(bio, _ffi.NULL) - else: - raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1") - - if crl == _ffi.NULL: - _raise_current_error() - - result = CRL.__new__(CRL) - result._crl = crl - return result - - - -def load_pkcs7_data(type, buffer): - """ - Load pkcs7 data from a buffer - - :param type: The file type (one of FILETYPE_PEM or FILETYPE_ASN1) - :param buffer: The buffer with the pkcs7 data. - :return: The PKCS7 object - """ - if isinstance(buffer, _text_type): - buffer = buffer.encode("ascii") - - bio = _new_mem_buf(buffer) - - if type == FILETYPE_PEM: - pkcs7 = _lib.PEM_read_bio_PKCS7(bio, _ffi.NULL, _ffi.NULL, _ffi.NULL) - elif type == FILETYPE_ASN1: - pass - else: - # TODO: This is untested. - _raise_current_error() - raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1") - - if pkcs7 == _ffi.NULL: - _raise_current_error() - - pypkcs7 = PKCS7.__new__(PKCS7) - pypkcs7._pkcs7 = _ffi.gc(pkcs7, _lib.PKCS7_free) - return pypkcs7 - - - -def load_pkcs12(buffer, passphrase=None): - """ - Load a PKCS12 object from a buffer - - :param buffer: The buffer the certificate is stored in - :param passphrase: (Optional) The password to decrypt the PKCS12 lump - :returns: The PKCS12 object - """ - if isinstance(buffer, _text_type): - buffer = buffer.encode("ascii") - - bio = _new_mem_buf(buffer) - - # Use null passphrase if passphrase is None or empty string. With PKCS#12 - # password based encryption no password and a zero length password are two - # different things, but OpenSSL implementation will try both to figure out - # which one works. - if not passphrase: - passphrase = _ffi.NULL - - p12 = _lib.d2i_PKCS12_bio(bio, _ffi.NULL) - if p12 == _ffi.NULL: - _raise_current_error() - p12 = _ffi.gc(p12, _lib.PKCS12_free) - - pkey = _ffi.new("EVP_PKEY**") - cert = _ffi.new("X509**") - cacerts = _ffi.new("Cryptography_STACK_OF_X509**") - - parse_result = _lib.PKCS12_parse(p12, passphrase, pkey, cert, cacerts) - if not parse_result: - _raise_current_error() - - cacerts = _ffi.gc(cacerts[0], _lib.sk_X509_free) - - # openssl 1.0.0 sometimes leaves an X509_check_private_key error in the - # queue for no particular reason. This error isn't interesting to anyone - # outside this function. It's not even interesting to us. Get rid of it. - try: - _raise_current_error() - except Error: - pass - - if pkey[0] == _ffi.NULL: - pykey = None - else: - pykey = PKey.__new__(PKey) - pykey._pkey = _ffi.gc(pkey[0], _lib.EVP_PKEY_free) - - if cert[0] == _ffi.NULL: - pycert = None - friendlyname = None - else: - pycert = X509.__new__(X509) - pycert._x509 = _ffi.gc(cert[0], _lib.X509_free) - - friendlyname_length = _ffi.new("int*") - friendlyname_buffer = _lib.X509_alias_get0(cert[0], friendlyname_length) - friendlyname = _ffi.buffer(friendlyname_buffer, friendlyname_length[0])[:] - if friendlyname_buffer == _ffi.NULL: - friendlyname = None - - pycacerts = [] - for i in range(_lib.sk_X509_num(cacerts)): - pycacert = X509.__new__(X509) - pycacert._x509 = _lib.sk_X509_value(cacerts, i) - pycacerts.append(pycacert) - if not pycacerts: - pycacerts = None - - pkcs12 = PKCS12.__new__(PKCS12) - pkcs12._pkey = pykey - pkcs12._cert = pycert - pkcs12._cacerts = pycacerts - pkcs12._friendlyname = friendlyname - return pkcs12 - - -def _initialize_openssl_threads(get_ident, Lock): - import _ssl - return - - locks = list(Lock() for n in range(_lib.CRYPTO_num_locks())) - - def locking_function(mode, index, filename, line): - if mode & _lib.CRYPTO_LOCK: - locks[index].acquire() - else: - locks[index].release() - - _lib.CRYPTO_set_id_callback( - _ffi.callback("unsigned long (*)(void)", get_ident)) - - _lib.CRYPTO_set_locking_callback( - _ffi.callback( - "void (*)(int, int, const char*, int)", locking_function)) - - -try: - from thread import get_ident - from threading import Lock -except ImportError: - pass -else: - _initialize_openssl_threads(get_ident, Lock) - del get_ident, Lock - -# There are no direct unit tests for this initialization. It is tested -# indirectly since it is necessary for functions like dump_privatekey when -# using encryption. -# -# Thus OpenSSL.test.test_crypto.FunctionTests.test_dump_privatekey_passphrase -# and some other similar tests may fail without this (though they may not if -# the Python runtime has already done some initialization of the underlying -# OpenSSL library (and is linked against the same one that cryptography is -# using)). -_lib.OpenSSL_add_all_algorithms() - -# This is similar but exercised mainly by exception_from_error_queue. It calls -# both ERR_load_crypto_strings() and ERR_load_SSL_strings(). -_lib.SSL_load_error_strings() diff --git a/OpenSSL/rand.py b/OpenSSL/rand.py deleted file mode 100644 index e754378ce..000000000 --- a/OpenSSL/rand.py +++ /dev/null @@ -1,180 +0,0 @@ -""" -PRNG management routines, thin wrappers. - -See the file RATIONALE for a short explanation of why this module was written. -""" - -from functools import partial - -from six import integer_types as _integer_types - -from OpenSSL._util import ( - ffi as _ffi, - lib as _lib, - exception_from_error_queue as _exception_from_error_queue) - - -class Error(Exception): - """ - An error occurred in an `OpenSSL.rand` API. - """ - -_raise_current_error = partial(_exception_from_error_queue, Error) - -_unspecified = object() - -_builtin_bytes = bytes - -def bytes(num_bytes): - """ - Get some random bytes as a string. - - :param num_bytes: The number of bytes to fetch - :return: A string of random bytes - """ - if not isinstance(num_bytes, _integer_types): - raise TypeError("num_bytes must be an integer") - - if num_bytes < 0: - raise ValueError("num_bytes must not be negative") - - result_buffer = _ffi.new("char[]", num_bytes) - result_code = _lib.RAND_bytes(result_buffer, num_bytes) - if result_code == -1: - # TODO: No tests for this code path. Triggering a RAND_bytes failure - # might involve supplying a custom ENGINE? That's hard. - _raise_current_error() - - return _ffi.buffer(result_buffer)[:] - - - -def add(buffer, entropy): - """ - Add data with a given entropy to the PRNG - - :param buffer: Buffer with random data - :param entropy: The entropy (in bytes) measurement of the buffer - :return: None - """ - if not isinstance(buffer, _builtin_bytes): - raise TypeError("buffer must be a byte string") - - if not isinstance(entropy, int): - raise TypeError("entropy must be an integer") - - # TODO Nothing tests this call actually being made, or made properly. - _lib.RAND_add(buffer, len(buffer), entropy) - - - -def seed(buffer): - """ - Alias for rand_add, with entropy equal to length - - :param buffer: Buffer with random data - :return: None - """ - if not isinstance(buffer, _builtin_bytes): - raise TypeError("buffer must be a byte string") - - # TODO Nothing tests this call actually being made, or made properly. - _lib.RAND_seed(buffer, len(buffer)) - - - -def status(): - """ - Retrieve the status of the PRNG - - :return: True if the PRNG is seeded enough, false otherwise - """ - return _lib.RAND_status() - - - -def egd(path, bytes=_unspecified): - """ - Query an entropy gathering daemon (EGD) for random data and add it to the - PRNG. I haven't found any problems when the socket is missing, the function - just returns 0. - - :param path: The path to the EGD socket - :param bytes: (optional) The number of bytes to read, default is 255 - :returns: The number of bytes read (NB: a value of 0 isn't necessarily an - error, check rand.status()) - """ - if not isinstance(path, _builtin_bytes): - raise TypeError("path must be a byte string") - - if bytes is _unspecified: - bytes = 255 - elif not isinstance(bytes, int): - raise TypeError("bytes must be an integer") - - return _lib.RAND_egd_bytes(path, bytes) - - - -def cleanup(): - """ - Erase the memory used by the PRNG. - - :return: None - """ - # TODO Nothing tests this call actually being made, or made properly. - _lib.RAND_cleanup() - - - -def load_file(filename, maxbytes=_unspecified): - """ - Seed the PRNG with data from a file - - :param filename: The file to read data from - :param maxbytes: (optional) The number of bytes to read, default is - to read the entire file - :return: The number of bytes read - """ - if not isinstance(filename, _builtin_bytes): - raise TypeError("filename must be a string") - - if maxbytes is _unspecified: - maxbytes = -1 - elif not isinstance(maxbytes, int): - raise TypeError("maxbytes must be an integer") - - return _lib.RAND_load_file(filename, maxbytes) - - - -def write_file(filename): - """ - Save PRNG state to a file - - :param filename: The file to write data to - :return: The number of bytes written - """ - if not isinstance(filename, _builtin_bytes): - raise TypeError("filename must be a string") - - return _lib.RAND_write_file(filename) - - -# TODO There are no tests for screen at all -def screen(): - """ - Add the current contents of the screen to the PRNG state. Availability: - Windows. - - :return: None - """ - _lib.RAND_screen() - -if getattr(_lib, 'RAND_screen', None) is None: - del screen - - -# TODO There are no tests for the RAND strings being loaded, whatever that -# means. -_lib.ERR_load_RAND_strings() diff --git a/OpenSSL/test/README b/OpenSSL/test/README deleted file mode 100644 index 8b3b3ffa6..000000000 --- a/OpenSSL/test/README +++ /dev/null @@ -1,8 +0,0 @@ -These tests are meant to be run using twisted's "trial" command. - -See http://twistedmatrix.com/trac/wiki/TwistedTrial - -For example... - -$ sudo python ./setup install -$ trial OpenSSL diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py deleted file mode 100644 index bbe5d050b..000000000 --- a/OpenSSL/test/test_crypto.py +++ /dev/null @@ -1,3283 +0,0 @@ -# Copyright (c) Jean-Paul Calderone -# See LICENSE file for details. - -""" -Unit tests for :py:mod:`OpenSSL.crypto`. -""" - -from unittest import main - -import os, re -from subprocess import PIPE, Popen -from datetime import datetime, timedelta - -from six import u, b, binary_type - -from OpenSSL.crypto import TYPE_RSA, TYPE_DSA, Error, PKey, PKeyType -from OpenSSL.crypto import X509, X509Type, X509Name, X509NameType -from OpenSSL.crypto import X509Store, X509StoreType, X509Req, X509ReqType -from OpenSSL.crypto import X509Extension, X509ExtensionType -from OpenSSL.crypto import load_certificate, load_privatekey -from OpenSSL.crypto import FILETYPE_PEM, FILETYPE_ASN1, FILETYPE_TEXT -from OpenSSL.crypto import dump_certificate, load_certificate_request -from OpenSSL.crypto import dump_certificate_request, dump_privatekey -from OpenSSL.crypto import PKCS7Type, load_pkcs7_data -from OpenSSL.crypto import PKCS12, PKCS12Type, load_pkcs12 -from OpenSSL.crypto import CRL, Revoked, load_crl -from OpenSSL.crypto import NetscapeSPKI, NetscapeSPKIType -from OpenSSL.crypto import ( - sign, verify, get_elliptic_curve, get_elliptic_curves) -from OpenSSL.test.util import EqualityTestsMixin, TestCase -from OpenSSL._util import native, lib - -def normalize_certificate_pem(pem): - return dump_certificate(FILETYPE_PEM, load_certificate(FILETYPE_PEM, pem)) - - -def normalize_privatekey_pem(pem): - return dump_privatekey(FILETYPE_PEM, load_privatekey(FILETYPE_PEM, pem)) - - -GOOD_CIPHER = "blowfish" -BAD_CIPHER = "zippers" - -GOOD_DIGEST = "MD5" -BAD_DIGEST = "monkeys" - -root_cert_pem = b("""-----BEGIN CERTIFICATE----- -MIIC7TCCAlagAwIBAgIIPQzE4MbeufQwDQYJKoZIhvcNAQEFBQAwWDELMAkGA1UE -BhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdU -ZXN0aW5nMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0EwIhgPMjAwOTAzMjUxMjM2 -NThaGA8yMDE3MDYxMTEyMzY1OFowWDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklM -MRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdUZXN0aW5nMRgwFgYDVQQDEw9U -ZXN0aW5nIFJvb3QgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPmaQumL -urpE527uSEHdL1pqcDRmWzu+98Y6YHzT/J7KWEamyMCNZ6fRW1JCR782UQ8a07fy -2xXsKy4WdKaxyG8CcatwmXvpvRQ44dSANMihHELpANTdyVp6DCysED6wkQFurHlF -1dshEaJw8b/ypDhmbVIo6Ci1xvCJqivbLFnbAgMBAAGjgbswgbgwHQYDVR0OBBYE -FINVdy1eIfFJDAkk51QJEo3IfgSuMIGIBgNVHSMEgYAwfoAUg1V3LV4h8UkMCSTn -VAkSjch+BK6hXKRaMFgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJJTDEQMA4GA1UE -BxMHQ2hpY2FnbzEQMA4GA1UEChMHVGVzdGluZzEYMBYGA1UEAxMPVGVzdGluZyBS -b290IENBggg9DMTgxt659DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GB -AGGCDazMJGoWNBpc03u6+smc95dEead2KlZXBATOdFT1VesY3+nUOqZhEhTGlDMi -hkgaZnzoIq/Uamidegk4hirsCT/R+6vsKAAxNTcBjUeZjlykCJWy5ojShGftXIKY -w/njVbKMXrvc83qmTdGl3TAM0fxQIpqgcglFLveEBgzn ------END CERTIFICATE----- -""") - -root_key_pem = b("""-----BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQD5mkLpi7q6ROdu7khB3S9aanA0Zls7vvfGOmB80/yeylhGpsjA -jWen0VtSQke/NlEPGtO38tsV7CsuFnSmschvAnGrcJl76b0UOOHUgDTIoRxC6QDU -3claegwsrBA+sJEBbqx5RdXbIRGicPG/8qQ4Zm1SKOgotcbwiaor2yxZ2wIDAQAB -AoGBAPCgMpmLxzwDaUmcFbTJUvlLW1hoxNNYSu2jIZm1k/hRAcE60JYwvBkgz3UB -yMEh0AtLxYe0bFk6EHah11tMUPgscbCq73snJ++8koUw+csk22G65hOs51bVb7Aa -6JBe67oLzdtvgCUFAA2qfrKzWRZzAdhUirQUZgySZk+Xq1pBAkEA/kZG0A6roTSM -BVnx7LnPfsycKUsTumorpXiylZJjTi9XtmzxhrYN6wgZlDOOwOLgSQhszGpxVoMD -u3gByT1b2QJBAPtL3mSKdvwRu/+40zaZLwvSJRxaj0mcE4BJOS6Oqs/hS1xRlrNk -PpQ7WJ4yM6ZOLnXzm2mKyxm50Mv64109FtMCQQDOqS2KkjHaLowTGVxwC0DijMfr -I9Lf8sSQk32J5VWCySWf5gGTfEnpmUa41gKTMJIbqZZLucNuDcOtzUaeWZlZAkA8 -ttXigLnCqR486JDPTi9ZscoZkZ+w7y6e/hH8t6d5Vjt48JVyfjPIaJY+km58LcN3 -6AWSeGAdtRFHVzR7oHjVAkB4hutvxiOeiIVQNBhM6RSI9aBPMI21DoX2JRoxvNW2 -cbvAhow217X9V0dVerEOKxnNYspXRrh36h7k4mQA+sDq ------END RSA PRIVATE KEY----- -""") - -server_cert_pem = b("""-----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIJAJn/HpR21r/8MA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJJTDEQMA4GA1UEBxMHQ2hpY2FnbzEQMA4GA1UEChMH -VGVzdGluZzEYMBYGA1UEAxMPVGVzdGluZyBSb290IENBMCIYDzIwMDkwMzI1MTIz -NzUzWhgPMjAxNzA2MTExMjM3NTNaMBgxFjAUBgNVBAMTDWxvdmVseSBzZXJ2ZXIw -gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAL6m+G653V0tpBC/OKl22VxOi2Cv -lK4TYu9LHSDP9uDVTe7V5D5Tl6qzFoRRx5pfmnkqT5B+W9byp2NU3FC5hLm5zSAr -b45meUhjEJ/ifkZgbNUjHdBIGP9MAQUHZa5WKdkGIJvGAvs8UzUqlr4TBWQIB24+ -lJ+Ukk/CRgasrYwdAgMBAAGjNjA0MB0GA1UdDgQWBBS4kC7Ij0W1TZXZqXQFAM2e -gKEG2DATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQUFAAOBgQBh30Li -dJ+NlxIOx5343WqIBka3UbsOb2kxWrbkVCrvRapCMLCASO4FqiKWM+L0VDBprqIp -2mgpFQ6FHpoIENGvJhdEKpptQ5i7KaGhnDNTfdy3x1+h852G99f1iyj0RmbuFcM8 -uzujnS8YXWvM7DM1Ilozk4MzPug8jzFp5uhKCQ== ------END CERTIFICATE----- -""") - -server_key_pem = normalize_privatekey_pem(b("""-----BEGIN RSA PRIVATE KEY----- -MIICWwIBAAKBgQC+pvhuud1dLaQQvzipdtlcTotgr5SuE2LvSx0gz/bg1U3u1eQ+ -U5eqsxaEUceaX5p5Kk+QflvW8qdjVNxQuYS5uc0gK2+OZnlIYxCf4n5GYGzVIx3Q -SBj/TAEFB2WuVinZBiCbxgL7PFM1Kpa+EwVkCAduPpSflJJPwkYGrK2MHQIDAQAB -AoGAbwuZ0AR6JveahBaczjfnSpiFHf+mve2UxoQdpyr6ROJ4zg/PLW5K/KXrC48G -j6f3tXMrfKHcpEoZrQWUfYBRCUsGD5DCazEhD8zlxEHahIsqpwA0WWssJA2VOLEN -j6DuV2pCFbw67rfTBkTSo32ahfXxEKev5KswZk0JIzH3ooECQQDgzS9AI89h0gs8 -Dt+1m11Rzqo3vZML7ZIyGApUzVan+a7hbc33nbGRkAXjHaUBJO31it/H6dTO+uwX -msWwNG5ZAkEA2RyFKs5xR5USTFaKLWCgpH/ydV96KPOpBND7TKQx62snDenFNNbn -FwwOhpahld+vqhYk+pfuWWUpQciE+Bu7ZQJASjfT4sQv4qbbKK/scePicnDdx9th -4e1EeB9xwb+tXXXUo/6Bor/AcUNwfiQ6Zt9PZOK9sR3lMZSsP7rMi7kzuQJABie6 -1sXXjFH7nNJvRG4S39cIxq8YRYTy68II/dlB2QzGpKxV/POCxbJ/zu0CU79tuYK7 -NaeNCFfH3aeTrX0LyQJAMBWjWmeKM2G2sCExheeQK0ROnaBC8itCECD4Jsve4nqf -r50+LF74iLXFwqysVCebPKMOpDWp/qQ1BbJQIPs7/A== ------END RSA PRIVATE KEY----- -""")) - -client_cert_pem = b("""-----BEGIN CERTIFICATE----- -MIICJjCCAY+gAwIBAgIJAKxpFI5lODkjMA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJJTDEQMA4GA1UEBxMHQ2hpY2FnbzEQMA4GA1UEChMH -VGVzdGluZzEYMBYGA1UEAxMPVGVzdGluZyBSb290IENBMCIYDzIwMDkwMzI1MTIz -ODA1WhgPMjAxNzA2MTExMjM4MDVaMBYxFDASBgNVBAMTC3VnbHkgY2xpZW50MIGf -MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAZh/SRtNm5ntMT4qb6YzEpTroMlq2 -rn+GrRHRiZ+xkCw/CGNhbtPir7/QxaUj26BSmQrHw1bGKEbPsWiW7bdXSespl+xK -iku4G/KvnnmWdeJHqsiXeUZtqurMELcPQAw9xPHEuhqqUJvvEoMTsnCEqGM+7Dtb -oCRajYyHfluARQIDAQABozYwNDAdBgNVHQ4EFgQUNQB+qkaOaEVecf1J3TTUtAff -0fAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQEFBQADgYEAyv/Jh7gM -Q3OHvmsFEEvRI+hsW8y66zK4K5de239Y44iZrFYkt7Q5nBPMEWDj4F2hLYWL/qtI -9Zdr0U4UDCU9SmmGYh4o7R4TZ5pGFvBYvjhHbkSFYFQXZxKUi+WUxplP6I0wr2KJ -PSTJCjJOn3xo2NTKRgV1gaoTf2EhL+RG8TQ= ------END CERTIFICATE----- -""") - -client_key_pem = normalize_privatekey_pem(b("""-----BEGIN RSA PRIVATE KEY----- -MIICXgIBAAKBgQDAZh/SRtNm5ntMT4qb6YzEpTroMlq2rn+GrRHRiZ+xkCw/CGNh -btPir7/QxaUj26BSmQrHw1bGKEbPsWiW7bdXSespl+xKiku4G/KvnnmWdeJHqsiX -eUZtqurMELcPQAw9xPHEuhqqUJvvEoMTsnCEqGM+7DtboCRajYyHfluARQIDAQAB -AoGATkZ+NceY5Glqyl4mD06SdcKfV65814vg2EL7V9t8+/mi9rYL8KztSXGlQWPX -zuHgtRoMl78yQ4ZJYOBVo+nsx8KZNRCEBlE19bamSbQLCeQMenWnpeYyQUZ908gF -h6L9qsFVJepgA9RDgAjyDoS5CaWCdCCPCH2lDkdcqC54SVUCQQDseuduc4wi8h4t -V8AahUn9fn9gYfhoNuM0gdguTA0nPLVWz4hy1yJiWYQe0H7NLNNTmCKiLQaJpAbb -TC6vE8C7AkEA0Ee8CMJUc20BnGEmxwgWcVuqFWaKCo8jTH1X38FlATUsyR3krjW2 -dL3yDD9NwHxsYP7nTKp/U8MV7U9IBn4y/wJBAJl7H0/BcLeRmuJk7IqJ7b635iYB -D/9beFUw3MUXmQXZUfyYz39xf6CDZsu1GEdEC5haykeln3Of4M9d/4Kj+FcCQQCY -si6xwT7GzMDkk/ko684AV3KPc/h6G0yGtFIrMg7J3uExpR/VdH2KgwMkZXisSMvw -JJEQjOMCVsEJlRk54WWjAkEAzoZNH6UhDdBK5F38rVt/y4SEHgbSfJHIAmPS32Kq -f6GGcfNpip0Uk7q7udTKuX7Q/buZi/C4YW7u3VKAquv9NA== ------END RSA PRIVATE KEY----- -""")) - -cleartextCertificatePEM = b("""-----BEGIN CERTIFICATE----- -MIIC7TCCAlagAwIBAgIIPQzE4MbeufQwDQYJKoZIhvcNAQEFBQAwWDELMAkGA1UE -BhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdU -ZXN0aW5nMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0EwIhgPMjAwOTAzMjUxMjM2 -NThaGA8yMDE3MDYxMTEyMzY1OFowWDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklM -MRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdUZXN0aW5nMRgwFgYDVQQDEw9U -ZXN0aW5nIFJvb3QgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPmaQumL -urpE527uSEHdL1pqcDRmWzu+98Y6YHzT/J7KWEamyMCNZ6fRW1JCR782UQ8a07fy -2xXsKy4WdKaxyG8CcatwmXvpvRQ44dSANMihHELpANTdyVp6DCysED6wkQFurHlF -1dshEaJw8b/ypDhmbVIo6Ci1xvCJqivbLFnbAgMBAAGjgbswgbgwHQYDVR0OBBYE -FINVdy1eIfFJDAkk51QJEo3IfgSuMIGIBgNVHSMEgYAwfoAUg1V3LV4h8UkMCSTn -VAkSjch+BK6hXKRaMFgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJJTDEQMA4GA1UE -BxMHQ2hpY2FnbzEQMA4GA1UEChMHVGVzdGluZzEYMBYGA1UEAxMPVGVzdGluZyBS -b290IENBggg9DMTgxt659DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GB -AGGCDazMJGoWNBpc03u6+smc95dEead2KlZXBATOdFT1VesY3+nUOqZhEhTGlDMi -hkgaZnzoIq/Uamidegk4hirsCT/R+6vsKAAxNTcBjUeZjlykCJWy5ojShGftXIKY -w/njVbKMXrvc83qmTdGl3TAM0fxQIpqgcglFLveEBgzn ------END CERTIFICATE----- -""") - -cleartextPrivateKeyPEM = normalize_privatekey_pem(b("""\ ------BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQD5mkLpi7q6ROdu7khB3S9aanA0Zls7vvfGOmB80/yeylhGpsjA -jWen0VtSQke/NlEPGtO38tsV7CsuFnSmschvAnGrcJl76b0UOOHUgDTIoRxC6QDU -3claegwsrBA+sJEBbqx5RdXbIRGicPG/8qQ4Zm1SKOgotcbwiaor2yxZ2wIDAQAB -AoGBAPCgMpmLxzwDaUmcFbTJUvlLW1hoxNNYSu2jIZm1k/hRAcE60JYwvBkgz3UB -yMEh0AtLxYe0bFk6EHah11tMUPgscbCq73snJ++8koUw+csk22G65hOs51bVb7Aa -6JBe67oLzdtvgCUFAA2qfrKzWRZzAdhUirQUZgySZk+Xq1pBAkEA/kZG0A6roTSM -BVnx7LnPfsycKUsTumorpXiylZJjTi9XtmzxhrYN6wgZlDOOwOLgSQhszGpxVoMD -u3gByT1b2QJBAPtL3mSKdvwRu/+40zaZLwvSJRxaj0mcE4BJOS6Oqs/hS1xRlrNk -PpQ7WJ4yM6ZOLnXzm2mKyxm50Mv64109FtMCQQDOqS2KkjHaLowTGVxwC0DijMfr -I9Lf8sSQk32J5VWCySWf5gGTfEnpmUa41gKTMJIbqZZLucNuDcOtzUaeWZlZAkA8 -ttXigLnCqR486JDPTi9ZscoZkZ+w7y6e/hH8t6d5Vjt48JVyfjPIaJY+km58LcN3 -6AWSeGAdtRFHVzR7oHjVAkB4hutvxiOeiIVQNBhM6RSI9aBPMI21DoX2JRoxvNW2 -cbvAhow217X9V0dVerEOKxnNYspXRrh36h7k4mQA+sDq ------END RSA PRIVATE KEY----- -""")) - -cleartextCertificateRequestPEM = b("""-----BEGIN CERTIFICATE REQUEST----- -MIIBnjCCAQcCAQAwXjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQH -EwdDaGljYWdvMRcwFQYDVQQKEw5NeSBDb21wYW55IEx0ZDEXMBUGA1UEAxMORnJl -ZGVyaWNrIERlYW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANp6Y17WzKSw -BsUWkXdqg6tnXy8H8hA1msCMWpc+/2KJ4mbv5NyD6UD+/SqagQqulPbF/DFea9nA -E0zhmHJELcM8gUTIlXv/cgDWnmK4xj8YkjVUiCdqKRAKeuzLG1pGmwwF5lGeJpXN -xQn5ecR0UYSOWj6TTGXB9VyUMQzCClcBAgMBAAGgADANBgkqhkiG9w0BAQUFAAOB -gQAAJGuF/R/GGbeC7FbFW+aJgr9ee0Xbl6nlhu7pTe67k+iiKT2dsl2ti68MVTnu -Vrb3HUNqOkiwsJf6kCtq5oPn3QVYzTa76Dt2y3Rtzv6boRSlmlfrgS92GNma8JfR -oICQk3nAudi6zl1Dix3BCv1pUp5KMtGn3MeDEi6QFGy2rA== ------END CERTIFICATE REQUEST----- -""") - -encryptedPrivateKeyPEM = b("""-----BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,9573604A18579E9E - -SHOho56WxDkT0ht10UTeKc0F5u8cqIa01kzFAmETw0MAs8ezYtK15NPdCXUm3X/2 -a17G7LSF5bkxOgZ7vpXyMzun/owrj7CzvLxyncyEFZWvtvzaAhPhvTJtTIB3kf8B -8+qRcpTGK7NgXEgYBW5bj1y4qZkD4zCL9o9NQzsKI3Ie8i0239jsDOWR38AxjXBH -mGwAQ4Z6ZN5dnmM4fhMIWsmFf19sNyAML4gHenQCHhmXbjXeVq47aC2ProInJbrm -+00TcisbAQ40V9aehVbcDKtS4ZbMVDwncAjpXpcncC54G76N6j7F7wL7L/FuXa3A -fvSVy9n2VfF/pJ3kYSflLHH2G/DFxjF7dl0GxhKPxJjp3IJi9VtuvmN9R2jZWLQF -tfC8dXgy/P9CfFQhlinqBTEwgH0oZ/d4k4NVFDSdEMaSdmBAjlHpc+Vfdty3HVnV -rKXj//wslsFNm9kIwJGIgKUa/n2jsOiydrsk1mgH7SmNCb3YHgZhbbnq0qLat/HC -gHDt3FHpNQ31QzzL3yrenFB2L9osIsnRsDTPFNi4RX4SpDgNroxOQmyzCCV6H+d4 -o1mcnNiZSdxLZxVKccq0AfRpHqpPAFnJcQHP6xyT9MZp6fBa0XkxDnt9kNU8H3Qw -7SJWZ69VXjBUzMlQViLuaWMgTnL+ZVyFZf9hTF7U/ef4HMLMAVNdiaGG+G+AjCV/ -MbzjS007Oe4qqBnCWaFPSnJX6uLApeTbqAxAeyCql56ULW5x6vDMNC3dwjvS/CEh -11n8RkgFIQA0AhuKSIg3CbuartRsJnWOLwgLTzsrKYL4yRog1RJrtw== ------END RSA PRIVATE KEY----- -""") - -encryptedPrivateKeyPEMPassphrase = b("foobar") - -# Some PKCS#7 stuff. Generated with the openssl command line: -# -# openssl crl2pkcs7 -inform pem -outform pem -certfile s.pem -nocrl -# -# with a certificate and key (but the key should be irrelevant) in s.pem -pkcs7Data = b("""\ ------BEGIN PKCS7----- -MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExADALBgkqhkiG9w0BBwGgggMKMIID -BjCCAm+gAwIBAgIBATANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzERMA8G -A1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQDExtN -MkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5ncHNA -cG9zdDEuY29tMB4XDTAwMDkxMDA5NTEzMFoXDTAyMDkxMDA5NTEzMFowUzELMAkG -A1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwlsb2NhbGhvc3Qx -HTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tMFwwDQYJKoZIhvcNAQEBBQAD -SwAwSAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh5kwI -zOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAaOCAQQwggEAMAkGA1UdEwQCMAAw -LAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0G -A1UdDgQWBBTPhIKSvnsmYsBVNWjj0m3M2z0qVTCBpQYDVR0jBIGdMIGagBT7hyNp -65w6kxXlxb8pUU/+7Sg4AaF/pH0wezELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0y -Q3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlwdG8g -Q2VydGlmaWNhdGUgTWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5uZ3BzQHBvc3QxLmNv -bYIBADANBgkqhkiG9w0BAQQFAAOBgQA7/CqT6PoHycTdhEStWNZde7M/2Yc6BoJu -VwnW8YxGO8Sn6UJ4FeffZNcYZddSDKosw8LtPOeWoK3JINjAk5jiPQ2cww++7QGG -/g5NDjxFZNDJP1dGiLAxPW6JXwov4v0FmdzfLOZ01jDcgQQZqEpYlgpuI5JEWUQ9 -Ho4EzbYCOaEAMQA= ------END PKCS7----- -""") - -crlData = b("""\ ------BEGIN X509 CRL----- -MIIBWzCBxTANBgkqhkiG9w0BAQQFADBYMQswCQYDVQQGEwJVUzELMAkGA1UECBMC -SUwxEDAOBgNVBAcTB0NoaWNhZ28xEDAOBgNVBAoTB1Rlc3RpbmcxGDAWBgNVBAMT -D1Rlc3RpbmcgUm9vdCBDQRcNMDkwNzI2MDQzNDU2WhcNMTIwOTI3MDI0MTUyWjA8 -MBUCAgOrGA8yMDA5MDcyNTIzMzQ1NlowIwICAQAYDzIwMDkwNzI1MjMzNDU2WjAM -MAoGA1UdFQQDCgEEMA0GCSqGSIb3DQEBBAUAA4GBAEBt7xTs2htdD3d4ErrcGAw1 -4dKcVnIWTutoI7xxen26Wwvh8VCsT7i/UeP+rBl9rC/kfjWjzQk3/zleaarGTpBT -0yp4HXRFFoRhhSE/hP+eteaPXRgrsNRLHe9ZDd69wmh7J1wMDb0m81RG7kqcbsid -vrzEeLDRiiPl92dyyWmu ------END X509 CRL----- -""") - - -# A broken RSA private key which can be used to test the error path through -# PKey.check. -inconsistentPrivateKeyPEM = b("""-----BEGIN RSA PRIVATE KEY----- -MIIBPAIBAAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh -5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEaAQJBAIqm/bz4NA1H++Vx5Ewx -OcKp3w19QSaZAwlGRtsUxrP7436QjnREM3Bm8ygU11BjkPVmtrKm6AayQfCHqJoT -zIECIQDW0BoMoL0HOYM/mrTLhaykYAVqgIeJsPjvkEhTFXWBuQIhAM3deFAvWNu4 -nklUQ37XsCT2c9tmNt1LAT+slG2JOTTRAiAuXDtC/m3NYVwyHfFm+zKHRzHkClk2 -HjubeEgjpj32AQIhAJqMGTaZVOwevTXvvHwNeH+vRWsAYU/gbx+OQB+7VOcBAiEA -oolb6NMg/R3enNPvS1O4UU1H8wpaF77L4yiSWlE0p4w= ------END RSA PRIVATE KEY----- -""") - -# certificate with NULL bytes in subjectAltName and common name - -nulbyteSubjectAltNamePEM = b("""-----BEGIN CERTIFICATE----- -MIIE2DCCA8CgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBxTELMAkGA1UEBhMCVVMx -DzANBgNVBAgMBk9yZWdvbjESMBAGA1UEBwwJQmVhdmVydG9uMSMwIQYDVQQKDBpQ -eXRob24gU29mdHdhcmUgRm91bmRhdGlvbjEgMB4GA1UECwwXUHl0aG9uIENvcmUg -RGV2ZWxvcG1lbnQxJDAiBgNVBAMMG251bGwucHl0aG9uLm9yZwBleGFtcGxlLm9y -ZzEkMCIGCSqGSIb3DQEJARYVcHl0aG9uLWRldkBweXRob24ub3JnMB4XDTEzMDgw -NzEzMTE1MloXDTEzMDgwNzEzMTI1MlowgcUxCzAJBgNVBAYTAlVTMQ8wDQYDVQQI -DAZPcmVnb24xEjAQBgNVBAcMCUJlYXZlcnRvbjEjMCEGA1UECgwaUHl0aG9uIFNv -ZnR3YXJlIEZvdW5kYXRpb24xIDAeBgNVBAsMF1B5dGhvbiBDb3JlIERldmVsb3Bt -ZW50MSQwIgYDVQQDDBtudWxsLnB5dGhvbi5vcmcAZXhhbXBsZS5vcmcxJDAiBgkq -hkiG9w0BCQEWFXB5dGhvbi1kZXZAcHl0aG9uLm9yZzCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBALXq7cn7Rn1vO3aA3TrzA5QLp6bb7B3f/yN0CJ2XFj+j -pHs+Gw6WWSUDpybiiKnPec33BFawq3kyblnBMjBU61ioy5HwQqVkJ8vUVjGIUq3P -vX/wBmQfzCe4o4uM89gpHyUL9UYGG8oCRa17dgqcv7u5rg0Wq2B1rgY+nHwx3JIv -KRrgSwyRkGzpN8WQ1yrXlxWjgI9de0mPVDDUlywcWze1q2kwaEPTM3hLAmD1PESA -oY/n8A/RXoeeRs9i/Pm/DGUS8ZPINXk/yOzsR/XvvkTVroIeLZqfmFpnZeF0cHzL -08LODkVJJ9zjLdT7SA4vnne4FEbAxDbKAq5qkYzaL4UCAwEAAaOB0DCBzTAMBgNV -HRMBAf8EAjAAMB0GA1UdDgQWBBSIWlXAUv9hzVKjNQ/qWpwkOCL3XDALBgNVHQ8E -BAMCBeAwgZAGA1UdEQSBiDCBhYIeYWx0bnVsbC5weXRob24ub3JnAGV4YW1wbGUu -Y29tgSBudWxsQHB5dGhvbi5vcmcAdXNlckBleGFtcGxlLm9yZ4YpaHR0cDovL251 -bGwucHl0aG9uLm9yZwBodHRwOi8vZXhhbXBsZS5vcmeHBMAAAgGHECABDbgAAAAA -AAAAAAAAAAEwDQYJKoZIhvcNAQEFBQADggEBAKxPRe99SaghcI6IWT7UNkJw9aO9 -i9eo0Fj2MUqxpKbdb9noRDy2CnHWf7EIYZ1gznXPdwzSN4YCjV5d+Q9xtBaowT0j -HPERs1ZuytCNNJTmhyqZ8q6uzMLoht4IqH/FBfpvgaeC5tBTnTT0rD5A/olXeimk -kX4LxlEx5RAvpGB2zZVRGr6LobD9rVK91xuHYNIxxxfEGE8tCCWjp0+3ksri9SXx -VHWBnbM9YaL32u3hxm8sYB/Yb8WSBavJCWJJqRStVRHM1koZlJmXNx2BX4vPo6iW -RFEIPQsFZRLrtnCAiEhyT8bC2s/Njlu6ly9gtJZWSV46Q3ZjBL4q9sHKqZQ= ------END CERTIFICATE-----""") - - -class X509ExtTests(TestCase): - """ - Tests for :py:class:`OpenSSL.crypto.X509Extension`. - """ - - def setUp(self): - """ - Create a new private key and start a certificate request (for a test - method to finish in one way or another). - """ - super(X509ExtTests, self).setUp() - # Basic setup stuff to generate a certificate - self.pkey = PKey() - self.pkey.generate_key(TYPE_RSA, 384) - self.req = X509Req() - self.req.set_pubkey(self.pkey) - # Authority good you have. - self.req.get_subject().commonName = "Yoda root CA" - self.x509 = X509() - self.subject = self.x509.get_subject() - self.subject.commonName = self.req.get_subject().commonName - self.x509.set_issuer(self.subject) - self.x509.set_pubkey(self.pkey) - now = b(datetime.now().strftime("%Y%m%d%H%M%SZ")) - expire = b((datetime.now() + timedelta(days=100)).strftime("%Y%m%d%H%M%SZ")) - self.x509.set_notBefore(now) - self.x509.set_notAfter(expire) - - - def tearDown(self): - """ - Forget all of the pyOpenSSL objects so they can be garbage collected, - their memory released, and not interfere with the leak detection code. - """ - self.pkey = self.req = self.x509 = self.subject = None - super(X509ExtTests, self).tearDown() - - - def test_str(self): - """ - The string representation of :py:class:`X509Extension` instances as returned by - :py:data:`str` includes stuff. - """ - # This isn't necessarily the best string representation. Perhaps it - # will be changed/improved in the future. - self.assertEquals( - str(X509Extension(b('basicConstraints'), True, b('CA:false'))), - 'CA:FALSE') - - - def test_type(self): - """ - :py:class:`X509Extension` and :py:class:`X509ExtensionType` refer to the same type object - and can be used to create instances of that type. - """ - self.assertIdentical(X509Extension, X509ExtensionType) - self.assertConsistentType( - X509Extension, - 'X509Extension', b('basicConstraints'), True, b('CA:true')) - - - def test_construction(self): - """ - :py:class:`X509Extension` accepts an extension type name, a critical flag, - and an extension value and returns an :py:class:`X509ExtensionType` instance. - """ - basic = X509Extension(b('basicConstraints'), True, b('CA:true')) - self.assertTrue( - isinstance(basic, X509ExtensionType), - "%r is of type %r, should be %r" % ( - basic, type(basic), X509ExtensionType)) - - comment = X509Extension( - b('nsComment'), False, b('pyOpenSSL unit test')) - self.assertTrue( - isinstance(comment, X509ExtensionType), - "%r is of type %r, should be %r" % ( - comment, type(comment), X509ExtensionType)) - - - def test_invalid_extension(self): - """ - :py:class:`X509Extension` raises something if it is passed a bad extension - name or value. - """ - self.assertRaises( - Error, X509Extension, b('thisIsMadeUp'), False, b('hi')) - self.assertRaises( - Error, X509Extension, b('basicConstraints'), False, b('blah blah')) - - # Exercise a weird one (an extension which uses the r2i method). This - # exercises the codepath that requires a non-NULL ctx to be passed to - # X509V3_EXT_nconf. It can't work now because we provide no - # configuration database. It might be made to work in the future. - self.assertRaises( - Error, X509Extension, b('proxyCertInfo'), True, - b('language:id-ppl-anyLanguage,pathlen:1,policy:text:AB')) - - - def test_get_critical(self): - """ - :py:meth:`X509ExtensionType.get_critical` returns the value of the - extension's critical flag. - """ - ext = X509Extension(b('basicConstraints'), True, b('CA:true')) - self.assertTrue(ext.get_critical()) - ext = X509Extension(b('basicConstraints'), False, b('CA:true')) - self.assertFalse(ext.get_critical()) - - - def test_get_short_name(self): - """ - :py:meth:`X509ExtensionType.get_short_name` returns a string giving the short - type name of the extension. - """ - ext = X509Extension(b('basicConstraints'), True, b('CA:true')) - self.assertEqual(ext.get_short_name(), b('basicConstraints')) - ext = X509Extension(b('nsComment'), True, b('foo bar')) - self.assertEqual(ext.get_short_name(), b('nsComment')) - - - def test_get_data(self): - """ - :py:meth:`X509Extension.get_data` returns a string giving the data of the - extension. - """ - ext = X509Extension(b('basicConstraints'), True, b('CA:true')) - # Expect to get back the DER encoded form of CA:true. - self.assertEqual(ext.get_data(), b('0\x03\x01\x01\xff')) - - - def test_get_data_wrong_args(self): - """ - :py:meth:`X509Extension.get_data` raises :py:exc:`TypeError` if passed any arguments. - """ - ext = X509Extension(b('basicConstraints'), True, b('CA:true')) - self.assertRaises(TypeError, ext.get_data, None) - self.assertRaises(TypeError, ext.get_data, "foo") - self.assertRaises(TypeError, ext.get_data, 7) - - - def test_unused_subject(self): - """ - The :py:data:`subject` parameter to :py:class:`X509Extension` may be provided for an - extension which does not use it and is ignored in this case. - """ - ext1 = X509Extension( - b('basicConstraints'), False, b('CA:TRUE'), subject=self.x509) - self.x509.add_extensions([ext1]) - self.x509.sign(self.pkey, 'sha1') - # This is a little lame. Can we think of a better way? - text = dump_certificate(FILETYPE_TEXT, self.x509) - self.assertTrue(b('X509v3 Basic Constraints:') in text) - self.assertTrue(b('CA:TRUE') in text) - - - def test_subject(self): - """ - If an extension requires a subject, the :py:data:`subject` parameter to - :py:class:`X509Extension` provides its value. - """ - ext3 = X509Extension( - b('subjectKeyIdentifier'), False, b('hash'), subject=self.x509) - self.x509.add_extensions([ext3]) - self.x509.sign(self.pkey, 'sha1') - text = dump_certificate(FILETYPE_TEXT, self.x509) - self.assertTrue(b('X509v3 Subject Key Identifier:') in text) - - - def test_missing_subject(self): - """ - If an extension requires a subject and the :py:data:`subject` parameter is - given no value, something happens. - """ - self.assertRaises( - Error, X509Extension, b('subjectKeyIdentifier'), False, b('hash')) - - - def test_invalid_subject(self): - """ - If the :py:data:`subject` parameter is given a value which is not an - :py:class:`X509` instance, :py:exc:`TypeError` is raised. - """ - for badObj in [True, object(), "hello", [], self]: - self.assertRaises( - TypeError, - X509Extension, - 'basicConstraints', False, 'CA:TRUE', subject=badObj) - - - def test_unused_issuer(self): - """ - The :py:data:`issuer` parameter to :py:class:`X509Extension` may be provided for an - extension which does not use it and is ignored in this case. - """ - ext1 = X509Extension( - b('basicConstraints'), False, b('CA:TRUE'), issuer=self.x509) - self.x509.add_extensions([ext1]) - self.x509.sign(self.pkey, 'sha1') - text = dump_certificate(FILETYPE_TEXT, self.x509) - self.assertTrue(b('X509v3 Basic Constraints:') in text) - self.assertTrue(b('CA:TRUE') in text) - - - def test_issuer(self): - """ - If an extension requires a issuer, the :py:data:`issuer` parameter to - :py:class:`X509Extension` provides its value. - """ - ext2 = X509Extension( - b('authorityKeyIdentifier'), False, b('issuer:always'), - issuer=self.x509) - self.x509.add_extensions([ext2]) - self.x509.sign(self.pkey, 'sha1') - text = dump_certificate(FILETYPE_TEXT, self.x509) - self.assertTrue(b('X509v3 Authority Key Identifier:') in text) - self.assertTrue(b('DirName:/CN=Yoda root CA') in text) - - - def test_missing_issuer(self): - """ - If an extension requires an issue and the :py:data:`issuer` parameter is given - no value, something happens. - """ - self.assertRaises( - Error, - X509Extension, - b('authorityKeyIdentifier'), False, - b('keyid:always,issuer:always')) - - - def test_invalid_issuer(self): - """ - If the :py:data:`issuer` parameter is given a value which is not an - :py:class:`X509` instance, :py:exc:`TypeError` is raised. - """ - for badObj in [True, object(), "hello", [], self]: - self.assertRaises( - TypeError, - X509Extension, - 'authorityKeyIdentifier', False, 'keyid:always,issuer:always', - issuer=badObj) - - - -class PKeyTests(TestCase): - """ - Unit tests for :py:class:`OpenSSL.crypto.PKey`. - """ - def test_type(self): - """ - :py:class:`PKey` and :py:class:`PKeyType` refer to the same type object - and can be used to create instances of that type. - """ - self.assertIdentical(PKey, PKeyType) - self.assertConsistentType(PKey, 'PKey') - - - def test_construction(self): - """ - :py:class:`PKey` takes no arguments and returns a new :py:class:`PKey` instance. - """ - self.assertRaises(TypeError, PKey, None) - key = PKey() - self.assertTrue( - isinstance(key, PKeyType), - "%r is of type %r, should be %r" % (key, type(key), PKeyType)) - - - def test_pregeneration(self): - """ - :py:attr:`PKeyType.bits` and :py:attr:`PKeyType.type` return :py:data:`0` before the key is - generated. :py:attr:`PKeyType.check` raises :py:exc:`TypeError` before the key is - generated. - """ - key = PKey() - self.assertEqual(key.type(), 0) - self.assertEqual(key.bits(), 0) - self.assertRaises(TypeError, key.check) - - - def test_failedGeneration(self): - """ - :py:meth:`PKeyType.generate_key` takes two arguments, the first giving the key - type as one of :py:data:`TYPE_RSA` or :py:data:`TYPE_DSA` and the second giving the - number of bits to generate. If an invalid type is specified or - generation fails, :py:exc:`Error` is raised. If an invalid number of bits is - specified, :py:exc:`ValueError` or :py:exc:`Error` is raised. - """ - key = PKey() - self.assertRaises(TypeError, key.generate_key) - self.assertRaises(TypeError, key.generate_key, 1, 2, 3) - self.assertRaises(TypeError, key.generate_key, "foo", "bar") - self.assertRaises(Error, key.generate_key, -1, 0) - - self.assertRaises(ValueError, key.generate_key, TYPE_RSA, -1) - self.assertRaises(ValueError, key.generate_key, TYPE_RSA, 0) - - # XXX RSA generation for small values of bits is fairly buggy in a wide - # range of OpenSSL versions. I need to figure out what the safe lower - # bound for a reasonable number of OpenSSL versions is and explicitly - # check for that in the wrapper. The failure behavior is typically an - # infinite loop inside OpenSSL. - - # self.assertRaises(Error, key.generate_key, TYPE_RSA, 2) - - # XXX DSA generation seems happy with any number of bits. The DSS - # says bits must be between 512 and 1024 inclusive. OpenSSL's DSA - # generator doesn't seem to care about the upper limit at all. For - # the lower limit, it uses 512 if anything smaller is specified. - # So, it doesn't seem possible to make generate_key fail for - # TYPE_DSA with a bits argument which is at least an int. - - # self.assertRaises(Error, key.generate_key, TYPE_DSA, -7) - - - def test_rsaGeneration(self): - """ - :py:meth:`PKeyType.generate_key` generates an RSA key when passed - :py:data:`TYPE_RSA` as a type and a reasonable number of bits. - """ - bits = 128 - key = PKey() - key.generate_key(TYPE_RSA, bits) - self.assertEqual(key.type(), TYPE_RSA) - self.assertEqual(key.bits(), bits) - self.assertTrue(key.check()) - - - def test_dsaGeneration(self): - """ - :py:meth:`PKeyType.generate_key` generates a DSA key when passed - :py:data:`TYPE_DSA` as a type and a reasonable number of bits. - """ - # 512 is a magic number. The DSS (Digital Signature Standard) - # allows a minimum of 512 bits for DSA. DSA_generate_parameters - # will silently promote any value below 512 to 512. - bits = 512 - key = PKey() - key.generate_key(TYPE_DSA, bits) - # self.assertEqual(key.type(), TYPE_DSA) - # self.assertEqual(key.bits(), bits) - # self.assertRaises(TypeError, key.check) - - - def test_regeneration(self): - """ - :py:meth:`PKeyType.generate_key` can be called multiple times on the same - key to generate new keys. - """ - key = PKey() - for type, bits in [(TYPE_RSA, 512), (TYPE_DSA, 576)]: - key.generate_key(type, bits) - self.assertEqual(key.type(), type) - self.assertEqual(key.bits(), bits) - - - def test_inconsistentKey(self): - """ - :py:`PKeyType.check` returns :py:exc:`Error` if the key is not consistent. - """ - key = load_privatekey(FILETYPE_PEM, inconsistentPrivateKeyPEM) - self.assertRaises(Error, key.check) - - - def test_check_wrong_args(self): - """ - :py:meth:`PKeyType.check` raises :py:exc:`TypeError` if called with any arguments. - """ - self.assertRaises(TypeError, PKey().check, None) - self.assertRaises(TypeError, PKey().check, object()) - self.assertRaises(TypeError, PKey().check, 1) - - - def test_check_public_key(self): - """ - :py:meth:`PKeyType.check` raises :py:exc:`TypeError` if only the public - part of the key is available. - """ - # A trick to get a public-only key - key = PKey() - key.generate_key(TYPE_RSA, 512) - cert = X509() - cert.set_pubkey(key) - pub = cert.get_pubkey() - self.assertRaises(TypeError, pub.check) - - - -class X509NameTests(TestCase): - """ - Unit tests for :py:class:`OpenSSL.crypto.X509Name`. - """ - def _x509name(self, **attrs): - # XXX There's no other way to get a new X509Name yet. - name = X509().get_subject() - attrs = list(attrs.items()) - # Make the order stable - order matters! - def key(attr): - return attr[1] - attrs.sort(key=key) - for k, v in attrs: - setattr(name, k, v) - return name - - - def test_type(self): - """ - The type of X509Name objects is :py:class:`X509NameType`. - """ - self.assertIdentical(X509Name, X509NameType) - self.assertEqual(X509NameType.__name__, 'X509Name') - self.assertTrue(isinstance(X509NameType, type)) - - name = self._x509name() - self.assertTrue( - isinstance(name, X509NameType), - "%r is of type %r, should be %r" % ( - name, type(name), X509NameType)) - - - def test_onlyStringAttributes(self): - """ - Attempting to set a non-:py:data:`str` attribute name on an :py:class:`X509NameType` - instance causes :py:exc:`TypeError` to be raised. - """ - name = self._x509name() - # Beyond these cases, you may also think that unicode should be - # rejected. Sorry, you're wrong. unicode is automatically converted to - # str outside of the control of X509Name, so there's no way to reject - # it. - - # Also, this used to test str subclasses, but that test is less relevant - # now that the implementation is in Python instead of C. Also PyPy - # automatically converts str subclasses to str when they are passed to - # setattr, so we can't test it on PyPy. Apparently CPython does this - # sometimes as well. - self.assertRaises(TypeError, setattr, name, None, "hello") - self.assertRaises(TypeError, setattr, name, 30, "hello") - - - def test_setInvalidAttribute(self): - """ - Attempting to set any attribute name on an :py:class:`X509NameType` instance for - which no corresponding NID is defined causes :py:exc:`AttributeError` to be - raised. - """ - name = self._x509name() - self.assertRaises(AttributeError, setattr, name, "no such thing", None) - - - def test_attributes(self): - """ - :py:class:`X509NameType` instances have attributes for each standard (?) - X509Name field. - """ - name = self._x509name() - name.commonName = "foo" - self.assertEqual(name.commonName, "foo") - self.assertEqual(name.CN, "foo") - name.CN = "baz" - self.assertEqual(name.commonName, "baz") - self.assertEqual(name.CN, "baz") - name.commonName = "bar" - self.assertEqual(name.commonName, "bar") - self.assertEqual(name.CN, "bar") - name.CN = "quux" - self.assertEqual(name.commonName, "quux") - self.assertEqual(name.CN, "quux") - - - def test_copy(self): - """ - :py:class:`X509Name` creates a new :py:class:`X509NameType` instance with all the same - attributes as an existing :py:class:`X509NameType` instance when called with - one. - """ - name = self._x509name(commonName="foo", emailAddress="bar@example.com") - - copy = X509Name(name) - self.assertEqual(copy.commonName, "foo") - self.assertEqual(copy.emailAddress, "bar@example.com") - - # Mutate the copy and ensure the original is unmodified. - copy.commonName = "baz" - self.assertEqual(name.commonName, "foo") - - # Mutate the original and ensure the copy is unmodified. - name.emailAddress = "quux@example.com" - self.assertEqual(copy.emailAddress, "bar@example.com") - - - def test_repr(self): - """ - :py:func:`repr` passed an :py:class:`X509NameType` instance should return a string - containing a description of the type and the NIDs which have been set - on it. - """ - name = self._x509name(commonName="foo", emailAddress="bar") - self.assertEqual( - repr(name), - "") - - - def test_comparison(self): - """ - :py:class:`X509NameType` instances should compare based on their NIDs. - """ - def _equality(a, b, assertTrue, assertFalse): - assertTrue(a == b, "(%r == %r) --> False" % (a, b)) - assertFalse(a != b) - assertTrue(b == a) - assertFalse(b != a) - - def assertEqual(a, b): - _equality(a, b, self.assertTrue, self.assertFalse) - - # Instances compare equal to themselves. - name = self._x509name() - assertEqual(name, name) - - # Empty instances should compare equal to each other. - assertEqual(self._x509name(), self._x509name()) - - # Instances with equal NIDs should compare equal to each other. - assertEqual(self._x509name(commonName="foo"), - self._x509name(commonName="foo")) - - # Instance with equal NIDs set using different aliases should compare - # equal to each other. - assertEqual(self._x509name(commonName="foo"), - self._x509name(CN="foo")) - - # Instances with more than one NID with the same values should compare - # equal to each other. - assertEqual(self._x509name(CN="foo", organizationalUnitName="bar"), - self._x509name(commonName="foo", OU="bar")) - - def assertNotEqual(a, b): - _equality(a, b, self.assertFalse, self.assertTrue) - - # Instances with different values for the same NID should not compare - # equal to each other. - assertNotEqual(self._x509name(CN="foo"), - self._x509name(CN="bar")) - - # Instances with different NIDs should not compare equal to each other. - assertNotEqual(self._x509name(CN="foo"), - self._x509name(OU="foo")) - - def _inequality(a, b, assertTrue, assertFalse): - assertTrue(a < b) - assertTrue(a <= b) - assertTrue(b > a) - assertTrue(b >= a) - assertFalse(a > b) - assertFalse(a >= b) - assertFalse(b < a) - assertFalse(b <= a) - - def assertLessThan(a, b): - _inequality(a, b, self.assertTrue, self.assertFalse) - - # An X509Name with a NID with a value which sorts less than the value - # of the same NID on another X509Name compares less than the other - # X509Name. - assertLessThan(self._x509name(CN="abc"), - self._x509name(CN="def")) - - def assertGreaterThan(a, b): - _inequality(a, b, self.assertFalse, self.assertTrue) - - # An X509Name with a NID with a value which sorts greater than the - # value of the same NID on another X509Name compares greater than the - # other X509Name. - assertGreaterThan(self._x509name(CN="def"), - self._x509name(CN="abc")) - - - def test_hash(self): - """ - :py:meth:`X509Name.hash` returns an integer hash based on the value of the - name. - """ - a = self._x509name(CN="foo") - b = self._x509name(CN="foo") - self.assertEqual(a.hash(), b.hash()) - a.CN = "bar" - self.assertNotEqual(a.hash(), b.hash()) - - - def test_der(self): - """ - :py:meth:`X509Name.der` returns the DER encoded form of the name. - """ - a = self._x509name(CN="foo", C="US") - self.assertEqual( - a.der(), - b('0\x1b1\x0b0\t\x06\x03U\x04\x06\x13\x02US' - '1\x0c0\n\x06\x03U\x04\x03\x13\x03foo')) - - - def test_get_components(self): - """ - :py:meth:`X509Name.get_components` returns a :py:data:`list` of - two-tuples of :py:data:`str` - giving the NIDs and associated values which make up the name. - """ - a = self._x509name() - self.assertEqual(a.get_components(), []) - a.CN = "foo" - self.assertEqual(a.get_components(), [(b("CN"), b("foo"))]) - a.organizationalUnitName = "bar" - self.assertEqual( - a.get_components(), - [(b("CN"), b("foo")), (b("OU"), b("bar"))]) - - - def test_load_nul_byte_attribute(self): - """ - An :py:class:`OpenSSL.crypto.X509Name` from an - :py:class:`OpenSSL.crypto.X509` instance loaded from a file can have a - NUL byte in the value of one of its attributes. - """ - cert = load_certificate(FILETYPE_PEM, nulbyteSubjectAltNamePEM) - subject = cert.get_subject() - self.assertEqual( - "null.python.org\x00example.org", subject.commonName) - - - def test_setAttributeFailure(self): - """ - If the value of an attribute cannot be set for some reason then - :py:class:`OpenSSL.crypto.Error` is raised. - """ - name = self._x509name() - # This value is too long - self.assertRaises(Error, setattr, name, "O", b"x" * 512) - - - -class _PKeyInteractionTestsMixin: - """ - Tests which involve another thing and a PKey. - """ - def signable(self): - """ - Return something with a :py:meth:`set_pubkey`, :py:meth:`set_pubkey`, - and :py:meth:`sign` method. - """ - raise NotImplementedError() - - - def test_signWithUngenerated(self): - """ - :py:meth:`X509Req.sign` raises :py:exc:`ValueError` when pass a - :py:class:`PKey` with no parts. - """ - request = self.signable() - key = PKey() - self.assertRaises(ValueError, request.sign, key, GOOD_DIGEST) - - - def test_signWithPublicKey(self): - """ - :py:meth:`X509Req.sign` raises :py:exc:`ValueError` when pass a - :py:class:`PKey` with no private part as the signing key. - """ - request = self.signable() - key = PKey() - key.generate_key(TYPE_RSA, 512) - request.set_pubkey(key) - pub = request.get_pubkey() - self.assertRaises(ValueError, request.sign, pub, GOOD_DIGEST) - - - def test_signWithUnknownDigest(self): - """ - :py:meth:`X509Req.sign` raises :py:exc:`ValueError` when passed a digest name which is - not known. - """ - request = self.signable() - key = PKey() - key.generate_key(TYPE_RSA, 512) - self.assertRaises(ValueError, request.sign, key, BAD_DIGEST) - - - def test_sign(self): - """ - :py:meth:`X509Req.sign` succeeds when passed a private key object and a valid - digest function. :py:meth:`X509Req.verify` can be used to check the signature. - """ - request = self.signable() - key = PKey() - key.generate_key(TYPE_RSA, 512) - request.set_pubkey(key) - request.sign(key, GOOD_DIGEST) - # If the type has a verify method, cover that too. - if getattr(request, 'verify', None) is not None: - pub = request.get_pubkey() - self.assertTrue(request.verify(pub)) - # Make another key that won't verify. - key = PKey() - key.generate_key(TYPE_RSA, 512) - self.assertRaises(Error, request.verify, key) - - - - -class X509ReqTests(TestCase, _PKeyInteractionTestsMixin): - """ - Tests for :py:class:`OpenSSL.crypto.X509Req`. - """ - def signable(self): - """ - Create and return a new :py:class:`X509Req`. - """ - return X509Req() - - - def test_type(self): - """ - :py:obj:`X509Req` and :py:obj:`X509ReqType` refer to the same type object and can be - used to create instances of that type. - """ - self.assertIdentical(X509Req, X509ReqType) - self.assertConsistentType(X509Req, 'X509Req') - - - def test_construction(self): - """ - :py:obj:`X509Req` takes no arguments and returns an :py:obj:`X509ReqType` instance. - """ - request = X509Req() - self.assertTrue( - isinstance(request, X509ReqType), - "%r is of type %r, should be %r" % (request, type(request), X509ReqType)) - - - def test_version(self): - """ - :py:obj:`X509ReqType.set_version` sets the X.509 version of the certificate - request. :py:obj:`X509ReqType.get_version` returns the X.509 version of - the certificate request. The initial value of the version is 0. - """ - request = X509Req() - self.assertEqual(request.get_version(), 0) - request.set_version(1) - self.assertEqual(request.get_version(), 1) - request.set_version(3) - self.assertEqual(request.get_version(), 3) - - - def test_version_wrong_args(self): - """ - :py:obj:`X509ReqType.set_version` raises :py:obj:`TypeError` if called with the wrong - number of arguments or with a non-:py:obj:`int` argument. - :py:obj:`X509ReqType.get_version` raises :py:obj:`TypeError` if called with any - arguments. - """ - request = X509Req() - self.assertRaises(TypeError, request.set_version) - self.assertRaises(TypeError, request.set_version, "foo") - self.assertRaises(TypeError, request.set_version, 1, 2) - self.assertRaises(TypeError, request.get_version, None) - - - def test_get_subject(self): - """ - :py:obj:`X509ReqType.get_subject` returns an :py:obj:`X509Name` for the subject of - the request and which is valid even after the request object is - otherwise dead. - """ - request = X509Req() - subject = request.get_subject() - self.assertTrue( - isinstance(subject, X509NameType), - "%r is of type %r, should be %r" % (subject, type(subject), X509NameType)) - subject.commonName = "foo" - self.assertEqual(request.get_subject().commonName, "foo") - del request - subject.commonName = "bar" - self.assertEqual(subject.commonName, "bar") - - - def test_get_subject_wrong_args(self): - """ - :py:obj:`X509ReqType.get_subject` raises :py:obj:`TypeError` if called with any - arguments. - """ - request = X509Req() - self.assertRaises(TypeError, request.get_subject, None) - - - def test_add_extensions(self): - """ - :py:obj:`X509Req.add_extensions` accepts a :py:obj:`list` of :py:obj:`X509Extension` - instances and adds them to the X509 request. - """ - request = X509Req() - request.add_extensions([ - X509Extension(b('basicConstraints'), True, b('CA:false'))]) - exts = request.get_extensions() - self.assertEqual(len(exts), 1) - self.assertEqual(exts[0].get_short_name(), b('basicConstraints')) - self.assertEqual(exts[0].get_critical(), 1) - self.assertEqual(exts[0].get_data(), b('0\x00')) - - - def test_get_extensions(self): - """ - :py:obj:`X509Req.get_extensions` returns a :py:obj:`list` of - extensions added to this X509 request. - """ - request = X509Req() - exts = request.get_extensions() - self.assertEqual(exts, []) - request.add_extensions([ - X509Extension(b('basicConstraints'), True, b('CA:true')), - X509Extension(b('keyUsage'), False, b('digitalSignature'))]) - exts = request.get_extensions() - self.assertEqual(len(exts), 2) - self.assertEqual(exts[0].get_short_name(), b('basicConstraints')) - self.assertEqual(exts[0].get_critical(), 1) - self.assertEqual(exts[0].get_data(), b('0\x03\x01\x01\xff')) - self.assertEqual(exts[1].get_short_name(), b('keyUsage')) - self.assertEqual(exts[1].get_critical(), 0) - self.assertEqual(exts[1].get_data(), b('\x03\x02\x07\x80')) - - - def test_add_extensions_wrong_args(self): - """ - :py:obj:`X509Req.add_extensions` raises :py:obj:`TypeError` if called with the wrong - number of arguments or with a non-:py:obj:`list`. Or it raises :py:obj:`ValueError` - if called with a :py:obj:`list` containing objects other than :py:obj:`X509Extension` - instances. - """ - request = X509Req() - self.assertRaises(TypeError, request.add_extensions) - self.assertRaises(TypeError, request.add_extensions, object()) - self.assertRaises(ValueError, request.add_extensions, [object()]) - self.assertRaises(TypeError, request.add_extensions, [], None) - - - def test_verify_wrong_args(self): - """ - :py:obj:`X509Req.verify` raises :py:obj:`TypeError` if called with zero - arguments or more than one argument or if passed anything other than a - :py:obj:`PKey` instance as its single argument. - """ - request = X509Req() - self.assertRaises(TypeError, request.verify) - self.assertRaises(TypeError, request.verify, object()) - self.assertRaises(TypeError, request.verify, PKey(), object()) - - - def test_verify_uninitialized_key(self): - """ - :py:obj:`X509Req.verify` raises :py:obj:`OpenSSL.crypto.Error` if called - with a :py:obj:`OpenSSL.crypto.PKey` which contains no key data. - """ - request = X509Req() - pkey = PKey() - self.assertRaises(Error, request.verify, pkey) - - - def test_verify_wrong_key(self): - """ - :py:obj:`X509Req.verify` raises :py:obj:`OpenSSL.crypto.Error` if called - with a :py:obj:`OpenSSL.crypto.PKey` which does not represent the public - part of the key which signed the request. - """ - request = X509Req() - pkey = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - request.sign(pkey, GOOD_DIGEST) - another_pkey = load_privatekey(FILETYPE_PEM, client_key_pem) - self.assertRaises(Error, request.verify, another_pkey) - - - def test_verify_success(self): - """ - :py:obj:`X509Req.verify` returns :py:obj:`True` if called with a - :py:obj:`OpenSSL.crypto.PKey` which represents the public part ofthe key - which signed the request. - """ - request = X509Req() - pkey = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - request.sign(pkey, GOOD_DIGEST) - self.assertEqual(True, request.verify(pkey)) - - - -class X509Tests(TestCase, _PKeyInteractionTestsMixin): - """ - Tests for :py:obj:`OpenSSL.crypto.X509`. - """ - pemData = cleartextCertificatePEM + cleartextPrivateKeyPEM - - extpem = """ ------BEGIN CERTIFICATE----- -MIIC3jCCAkegAwIBAgIJAJHFjlcCgnQzMA0GCSqGSIb3DQEBBQUAMEcxCzAJBgNV -BAYTAlNFMRUwEwYDVQQIEwxXZXN0ZXJib3R0b20xEjAQBgNVBAoTCUNhdGFsb2dp -eDENMAsGA1UEAxMEUm9vdDAeFw0wODA0MjIxNDQ1MzhaFw0wOTA0MjIxNDQ1Mzha -MFQxCzAJBgNVBAYTAlNFMQswCQYDVQQIEwJXQjEUMBIGA1UEChMLT3Blbk1ldGFk -aXIxIjAgBgNVBAMTGW5vZGUxLm9tMi5vcGVubWV0YWRpci5vcmcwgZ8wDQYJKoZI -hvcNAQEBBQADgY0AMIGJAoGBAPIcQMrwbk2nESF/0JKibj9i1x95XYAOwP+LarwT -Op4EQbdlI9SY+uqYqlERhF19w7CS+S6oyqx0DRZSk4Y9dZ9j9/xgm2u/f136YS1u -zgYFPvfUs6PqYLPSM8Bw+SjJ+7+2+TN+Tkiof9WP1cMjodQwOmdsiRbR0/J7+b1B -hec1AgMBAAGjgcQwgcEwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNT -TCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFIdHsBcMVVMbAO7j6NCj -03HgLnHaMB8GA1UdIwQYMBaAFL2h9Bf9Mre4vTdOiHTGAt7BRY/8MEYGA1UdEQQ/ -MD2CDSouZXhhbXBsZS5vcmeCESoub20yLmV4bWFwbGUuY29thwSC7wgKgRNvbTJA -b3Blbm1ldGFkaXIub3JnMA0GCSqGSIb3DQEBBQUAA4GBALd7WdXkp2KvZ7/PuWZA -MPlIxyjS+Ly11+BNE0xGQRp9Wz+2lABtpgNqssvU156+HkKd02rGheb2tj7MX9hG -uZzbwDAZzJPjzDQDD7d3cWsrVcfIdqVU7epHqIadnOF+X0ghJ39pAm6VVadnSXCt -WpOdIpB8KksUTCzV591Nr1wd ------END CERTIFICATE----- - """ - def signable(self): - """ - Create and return a new :py:obj:`X509`. - """ - return X509() - - - def test_type(self): - """ - :py:obj:`X509` and :py:obj:`X509Type` refer to the same type object and can be used - to create instances of that type. - """ - self.assertIdentical(X509, X509Type) - self.assertConsistentType(X509, 'X509') - - - def test_construction(self): - """ - :py:obj:`X509` takes no arguments and returns an instance of :py:obj:`X509Type`. - """ - certificate = X509() - self.assertTrue( - isinstance(certificate, X509Type), - "%r is of type %r, should be %r" % (certificate, - type(certificate), - X509Type)) - self.assertEqual(type(X509Type).__name__, 'type') - self.assertEqual(type(certificate).__name__, 'X509') - self.assertEqual(type(certificate), X509Type) - self.assertEqual(type(certificate), X509) - - - def test_get_version_wrong_args(self): - """ - :py:obj:`X509.get_version` raises :py:obj:`TypeError` if invoked with any arguments. - """ - cert = X509() - self.assertRaises(TypeError, cert.get_version, None) - - - def test_set_version_wrong_args(self): - """ - :py:obj:`X509.set_version` raises :py:obj:`TypeError` if invoked with the wrong number - of arguments or an argument not of type :py:obj:`int`. - """ - cert = X509() - self.assertRaises(TypeError, cert.set_version) - self.assertRaises(TypeError, cert.set_version, None) - self.assertRaises(TypeError, cert.set_version, 1, None) - - - def test_version(self): - """ - :py:obj:`X509.set_version` sets the certificate version number. - :py:obj:`X509.get_version` retrieves it. - """ - cert = X509() - cert.set_version(1234) - self.assertEquals(cert.get_version(), 1234) - - - def test_get_serial_number_wrong_args(self): - """ - :py:obj:`X509.get_serial_number` raises :py:obj:`TypeError` if invoked with any - arguments. - """ - cert = X509() - self.assertRaises(TypeError, cert.get_serial_number, None) - - - def test_serial_number(self): - """ - The serial number of an :py:obj:`X509Type` can be retrieved and modified with - :py:obj:`X509Type.get_serial_number` and :py:obj:`X509Type.set_serial_number`. - """ - certificate = X509() - self.assertRaises(TypeError, certificate.set_serial_number) - self.assertRaises(TypeError, certificate.set_serial_number, 1, 2) - self.assertRaises(TypeError, certificate.set_serial_number, "1") - self.assertRaises(TypeError, certificate.set_serial_number, 5.5) - self.assertEqual(certificate.get_serial_number(), 0) - certificate.set_serial_number(1) - self.assertEqual(certificate.get_serial_number(), 1) - certificate.set_serial_number(2 ** 32 + 1) - self.assertEqual(certificate.get_serial_number(), 2 ** 32 + 1) - certificate.set_serial_number(2 ** 64 + 1) - self.assertEqual(certificate.get_serial_number(), 2 ** 64 + 1) - certificate.set_serial_number(2 ** 128 + 1) - self.assertEqual(certificate.get_serial_number(), 2 ** 128 + 1) - - - def _setBoundTest(self, which): - """ - :py:obj:`X509Type.set_notBefore` takes a string in the format of an ASN1 - GENERALIZEDTIME and sets the beginning of the certificate's validity - period to it. - """ - certificate = X509() - set = getattr(certificate, 'set_not' + which) - get = getattr(certificate, 'get_not' + which) - - # Starts with no value. - self.assertEqual(get(), None) - - # GMT (Or is it UTC?) -exarkun - when = b("20040203040506Z") - set(when) - self.assertEqual(get(), when) - - # A plus two hours and thirty minutes offset - when = b("20040203040506+0530") - set(when) - self.assertEqual(get(), when) - - # A minus one hour fifteen minutes offset - when = b("20040203040506-0115") - set(when) - self.assertEqual(get(), when) - - # An invalid string results in a ValueError - self.assertRaises(ValueError, set, b("foo bar")) - - # The wrong number of arguments results in a TypeError. - self.assertRaises(TypeError, set) - self.assertRaises(TypeError, set, b("20040203040506Z"), b("20040203040506Z")) - self.assertRaises(TypeError, get, b("foo bar")) - - - # XXX ASN1_TIME (not GENERALIZEDTIME) - - def test_set_notBefore(self): - """ - :py:obj:`X509Type.set_notBefore` takes a string in the format of an ASN1 - GENERALIZEDTIME and sets the beginning of the certificate's validity - period to it. - """ - self._setBoundTest("Before") - - - def test_set_notAfter(self): - """ - :py:obj:`X509Type.set_notAfter` takes a string in the format of an ASN1 - GENERALIZEDTIME and sets the end of the certificate's validity period - to it. - """ - self._setBoundTest("After") - - - def test_get_notBefore(self): - """ - :py:obj:`X509Type.get_notBefore` returns a string in the format of an ASN1 - GENERALIZEDTIME even for certificates which store it as UTCTIME - internally. - """ - cert = load_certificate(FILETYPE_PEM, self.pemData) - self.assertEqual(cert.get_notBefore(), b("20090325123658Z")) - - - def test_get_notAfter(self): - """ - :py:obj:`X509Type.get_notAfter` returns a string in the format of an ASN1 - GENERALIZEDTIME even for certificates which store it as UTCTIME - internally. - """ - cert = load_certificate(FILETYPE_PEM, self.pemData) - self.assertEqual(cert.get_notAfter(), b("20170611123658Z")) - - - def test_gmtime_adj_notBefore_wrong_args(self): - """ - :py:obj:`X509Type.gmtime_adj_notBefore` raises :py:obj:`TypeError` if called with the - wrong number of arguments or a non-:py:obj:`int` argument. - """ - cert = X509() - self.assertRaises(TypeError, cert.gmtime_adj_notBefore) - self.assertRaises(TypeError, cert.gmtime_adj_notBefore, None) - self.assertRaises(TypeError, cert.gmtime_adj_notBefore, 123, None) - - - def test_gmtime_adj_notBefore(self): - """ - :py:obj:`X509Type.gmtime_adj_notBefore` changes the not-before timestamp to be - the current time plus the number of seconds passed in. - """ - cert = load_certificate(FILETYPE_PEM, self.pemData) - now = datetime.utcnow() + timedelta(seconds=100) - cert.gmtime_adj_notBefore(100) - self.assertEqual(cert.get_notBefore(), b(now.strftime("%Y%m%d%H%M%SZ"))) - - - def test_gmtime_adj_notAfter_wrong_args(self): - """ - :py:obj:`X509Type.gmtime_adj_notAfter` raises :py:obj:`TypeError` if called with the - wrong number of arguments or a non-:py:obj:`int` argument. - """ - cert = X509() - self.assertRaises(TypeError, cert.gmtime_adj_notAfter) - self.assertRaises(TypeError, cert.gmtime_adj_notAfter, None) - self.assertRaises(TypeError, cert.gmtime_adj_notAfter, 123, None) - - - def test_gmtime_adj_notAfter(self): - """ - :py:obj:`X509Type.gmtime_adj_notAfter` changes the not-after timestamp to be - the current time plus the number of seconds passed in. - """ - cert = load_certificate(FILETYPE_PEM, self.pemData) - now = datetime.utcnow() + timedelta(seconds=100) - cert.gmtime_adj_notAfter(100) - self.assertEqual(cert.get_notAfter(), b(now.strftime("%Y%m%d%H%M%SZ"))) - - - def test_has_expired_wrong_args(self): - """ - :py:obj:`X509Type.has_expired` raises :py:obj:`TypeError` if called with any - arguments. - """ - cert = X509() - self.assertRaises(TypeError, cert.has_expired, None) - - - def test_has_expired(self): - """ - :py:obj:`X509Type.has_expired` returns :py:obj:`True` if the certificate's not-after - time is in the past. - """ - cert = X509() - cert.gmtime_adj_notAfter(-1) - self.assertTrue(cert.has_expired()) - - - def test_has_not_expired(self): - """ - :py:obj:`X509Type.has_expired` returns :py:obj:`False` if the certificate's not-after - time is in the future. - """ - cert = X509() - cert.gmtime_adj_notAfter(2) - self.assertFalse(cert.has_expired()) - - - def test_digest(self): - """ - :py:obj:`X509.digest` returns a string giving ":"-separated hex-encoded words - of the digest of the certificate. - """ - cert = X509() - self.assertEqual( - # This is MD5 instead of GOOD_DIGEST because the digest algorithm - # actually matters to the assertion (ie, another arbitrary, good - # digest will not product the same digest). - cert.digest("MD5"), - b("A8:EB:07:F8:53:25:0A:F2:56:05:C5:A5:C4:C4:C7:15")) - - - def _extcert(self, pkey, extensions): - cert = X509() - cert.set_pubkey(pkey) - cert.get_subject().commonName = "Unit Tests" - cert.get_issuer().commonName = "Unit Tests" - when = b(datetime.now().strftime("%Y%m%d%H%M%SZ")) - cert.set_notBefore(when) - cert.set_notAfter(when) - - cert.add_extensions(extensions) - return load_certificate( - FILETYPE_PEM, dump_certificate(FILETYPE_PEM, cert)) - - - def test_extension_count(self): - """ - :py:obj:`X509.get_extension_count` returns the number of extensions that are - present in the certificate. - """ - pkey = load_privatekey(FILETYPE_PEM, client_key_pem) - ca = X509Extension(b('basicConstraints'), True, b('CA:FALSE')) - key = X509Extension(b('keyUsage'), True, b('digitalSignature')) - subjectAltName = X509Extension( - b('subjectAltName'), True, b('DNS:example.com')) - - # Try a certificate with no extensions at all. - c = self._extcert(pkey, []) - self.assertEqual(c.get_extension_count(), 0) - - # And a certificate with one - c = self._extcert(pkey, [ca]) - self.assertEqual(c.get_extension_count(), 1) - - # And a certificate with several - c = self._extcert(pkey, [ca, key, subjectAltName]) - self.assertEqual(c.get_extension_count(), 3) - - - def test_get_extension(self): - """ - :py:obj:`X509.get_extension` takes an integer and returns an :py:obj:`X509Extension` - corresponding to the extension at that index. - """ - pkey = load_privatekey(FILETYPE_PEM, client_key_pem) - ca = X509Extension(b('basicConstraints'), True, b('CA:FALSE')) - key = X509Extension(b('keyUsage'), True, b('digitalSignature')) - subjectAltName = X509Extension( - b('subjectAltName'), False, b('DNS:example.com')) - - cert = self._extcert(pkey, [ca, key, subjectAltName]) - - ext = cert.get_extension(0) - self.assertTrue(isinstance(ext, X509Extension)) - self.assertTrue(ext.get_critical()) - self.assertEqual(ext.get_short_name(), b('basicConstraints')) - - ext = cert.get_extension(1) - self.assertTrue(isinstance(ext, X509Extension)) - self.assertTrue(ext.get_critical()) - self.assertEqual(ext.get_short_name(), b('keyUsage')) - - ext = cert.get_extension(2) - self.assertTrue(isinstance(ext, X509Extension)) - self.assertFalse(ext.get_critical()) - self.assertEqual(ext.get_short_name(), b('subjectAltName')) - - self.assertRaises(IndexError, cert.get_extension, -1) - self.assertRaises(IndexError, cert.get_extension, 4) - self.assertRaises(TypeError, cert.get_extension, "hello") - - - def test_nullbyte_subjectAltName(self): - """ - The fields of a `subjectAltName` extension on an X509 may contain NUL - bytes and this value is reflected in the string representation of the - extension object. - """ - cert = load_certificate(FILETYPE_PEM, nulbyteSubjectAltNamePEM) - - ext = cert.get_extension(3) - self.assertEqual(ext.get_short_name(), b('subjectAltName')) - self.assertEqual( - b("DNS:altnull.python.org\x00example.com, " - "email:null@python.org\x00user@example.org, " - "URI:http://null.python.org\x00http://example.org, " - "IP Address:192.0.2.1, IP Address:2001:DB8:0:0:0:0:0:1\n"), - b(str(ext))) - - - def test_invalid_digest_algorithm(self): - """ - :py:obj:`X509.digest` raises :py:obj:`ValueError` if called with an unrecognized hash - algorithm. - """ - cert = X509() - self.assertRaises(ValueError, cert.digest, BAD_DIGEST) - - - def test_get_subject_wrong_args(self): - """ - :py:obj:`X509.get_subject` raises :py:obj:`TypeError` if called with any arguments. - """ - cert = X509() - self.assertRaises(TypeError, cert.get_subject, None) - - - def test_get_subject(self): - """ - :py:obj:`X509.get_subject` returns an :py:obj:`X509Name` instance. - """ - cert = load_certificate(FILETYPE_PEM, self.pemData) - subj = cert.get_subject() - self.assertTrue(isinstance(subj, X509Name)) - self.assertEquals( - subj.get_components(), - [(b('C'), b('US')), (b('ST'), b('IL')), (b('L'), b('Chicago')), - (b('O'), b('Testing')), (b('CN'), b('Testing Root CA'))]) - - - def test_set_subject_wrong_args(self): - """ - :py:obj:`X509.set_subject` raises a :py:obj:`TypeError` if called with the wrong - number of arguments or an argument not of type :py:obj:`X509Name`. - """ - cert = X509() - self.assertRaises(TypeError, cert.set_subject) - self.assertRaises(TypeError, cert.set_subject, None) - self.assertRaises(TypeError, cert.set_subject, cert.get_subject(), None) - - - def test_set_subject(self): - """ - :py:obj:`X509.set_subject` changes the subject of the certificate to the one - passed in. - """ - cert = X509() - name = cert.get_subject() - name.C = 'AU' - name.O = 'Unit Tests' - cert.set_subject(name) - self.assertEquals( - cert.get_subject().get_components(), - [(b('C'), b('AU')), (b('O'), b('Unit Tests'))]) - - - def test_get_issuer_wrong_args(self): - """ - :py:obj:`X509.get_issuer` raises :py:obj:`TypeError` if called with any arguments. - """ - cert = X509() - self.assertRaises(TypeError, cert.get_issuer, None) - - - def test_get_issuer(self): - """ - :py:obj:`X509.get_issuer` returns an :py:obj:`X509Name` instance. - """ - cert = load_certificate(FILETYPE_PEM, self.pemData) - subj = cert.get_issuer() - self.assertTrue(isinstance(subj, X509Name)) - comp = subj.get_components() - self.assertEquals( - comp, - [(b('C'), b('US')), (b('ST'), b('IL')), (b('L'), b('Chicago')), - (b('O'), b('Testing')), (b('CN'), b('Testing Root CA'))]) - - - def test_set_issuer_wrong_args(self): - """ - :py:obj:`X509.set_issuer` raises a :py:obj:`TypeError` if called with the wrong - number of arguments or an argument not of type :py:obj:`X509Name`. - """ - cert = X509() - self.assertRaises(TypeError, cert.set_issuer) - self.assertRaises(TypeError, cert.set_issuer, None) - self.assertRaises(TypeError, cert.set_issuer, cert.get_issuer(), None) - - - def test_set_issuer(self): - """ - :py:obj:`X509.set_issuer` changes the issuer of the certificate to the one - passed in. - """ - cert = X509() - name = cert.get_issuer() - name.C = 'AU' - name.O = 'Unit Tests' - cert.set_issuer(name) - self.assertEquals( - cert.get_issuer().get_components(), - [(b('C'), b('AU')), (b('O'), b('Unit Tests'))]) - - - def test_get_pubkey_uninitialized(self): - """ - When called on a certificate with no public key, :py:obj:`X509.get_pubkey` - raises :py:obj:`OpenSSL.crypto.Error`. - """ - cert = X509() - self.assertRaises(Error, cert.get_pubkey) - - - def test_subject_name_hash_wrong_args(self): - """ - :py:obj:`X509.subject_name_hash` raises :py:obj:`TypeError` if called with any - arguments. - """ - cert = X509() - self.assertRaises(TypeError, cert.subject_name_hash, None) - - - def test_subject_name_hash(self): - """ - :py:obj:`X509.subject_name_hash` returns the hash of the certificate's subject - name. - """ - cert = load_certificate(FILETYPE_PEM, self.pemData) - self.assertIn( - cert.subject_name_hash(), - [3350047874, # OpenSSL 0.9.8, MD5 - 3278919224, # OpenSSL 1.0.0, SHA1 - ]) - - - def test_get_signature_algorithm(self): - """ - :py:obj:`X509Type.get_signature_algorithm` returns a string which means - the algorithm used to sign the certificate. - """ - cert = load_certificate(FILETYPE_PEM, self.pemData) - self.assertEqual( - b("sha1WithRSAEncryption"), cert.get_signature_algorithm()) - - - def test_get_undefined_signature_algorithm(self): - """ - :py:obj:`X509Type.get_signature_algorithm` raises :py:obj:`ValueError` if the - signature algorithm is undefined or unknown. - """ - # This certificate has been modified to indicate a bogus OID in the - # signature algorithm field so that OpenSSL does not recognize it. - certPEM = b("""\ ------BEGIN CERTIFICATE----- -MIIC/zCCAmigAwIBAgIBATAGBgJ8BQUAMHsxCzAJBgNVBAYTAlNHMREwDwYDVQQK -EwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0ExJDAiBgNVBAMTG00yQ3J5 -cHRvIENlcnRpZmljYXRlIE1hc3RlcjEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0 -MS5jb20wHhcNMDAwOTEwMDk1MTMwWhcNMDIwOTEwMDk1MTMwWjBTMQswCQYDVQQG -EwJTRzERMA8GA1UEChMITTJDcnlwdG8xEjAQBgNVBAMTCWxvY2FsaG9zdDEdMBsG -CSqGSIb3DQEJARYObmdwc0Bwb3N0MS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBI -AkEArL57d26W9fNXvOhNlZzlPOACmvwOZ5AdNgLzJ1/MfsQQJ7hHVeHmTAjM664V -+fXvwUGJLziCeBo1ysWLRnl8CQIDAQABo4IBBDCCAQAwCQYDVR0TBAIwADAsBglg -hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O -BBYEFM+EgpK+eyZiwFU1aOPSbczbPSpVMIGlBgNVHSMEgZ0wgZqAFPuHI2nrnDqT -FeXFvylRT/7tKDgBoX+kfTB7MQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlw -dG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQDExtNMkNyeXB0byBDZXJ0 -aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tggEA -MA0GCSqGSIb3DQEBBAUAA4GBADv8KpPo+gfJxN2ERK1Y1l17sz/ZhzoGgm5XCdbx -jEY7xKfpQngV599k1xhl11IMqizDwu0855agrckg2MCTmOI9DZzDD77tAYb+Dk0O -PEVk0Mk/V0aIsDE9bolfCi/i/QWZ3N8s5nTWMNyBBBmoSliWCm4jkkRZRD0ejgTN -tgI5 ------END CERTIFICATE----- -""") - cert = load_certificate(FILETYPE_PEM, certPEM) - self.assertRaises(ValueError, cert.get_signature_algorithm) - - - -class X509StoreTests(TestCase): - """ - Test for :py:obj:`OpenSSL.crypto.X509Store`. - """ - def test_type(self): - """ - :py:obj:`X509StoreType` is a type object. - """ - self.assertIdentical(X509Store, X509StoreType) - self.assertConsistentType(X509Store, 'X509Store') - - - def test_add_cert_wrong_args(self): - store = X509Store() - self.assertRaises(TypeError, store.add_cert) - self.assertRaises(TypeError, store.add_cert, object()) - self.assertRaises(TypeError, store.add_cert, X509(), object()) - - - def test_add_cert(self): - """ - :py:obj:`X509Store.add_cert` adds a :py:obj:`X509` instance to the - certificate store. - """ - cert = load_certificate(FILETYPE_PEM, cleartextCertificatePEM) - store = X509Store() - store.add_cert(cert) - - - def test_add_cert_rejects_duplicate(self): - """ - :py:obj:`X509Store.add_cert` raises :py:obj:`OpenSSL.crypto.Error` if an - attempt is made to add the same certificate to the store more than once. - """ - cert = load_certificate(FILETYPE_PEM, cleartextCertificatePEM) - store = X509Store() - store.add_cert(cert) - self.assertRaises(Error, store.add_cert, cert) - - - -class PKCS12Tests(TestCase): - """ - Test for :py:obj:`OpenSSL.crypto.PKCS12` and :py:obj:`OpenSSL.crypto.load_pkcs12`. - """ - pemData = cleartextCertificatePEM + cleartextPrivateKeyPEM - - def test_type(self): - """ - :py:obj:`PKCS12Type` is a type object. - """ - self.assertIdentical(PKCS12, PKCS12Type) - self.assertConsistentType(PKCS12, 'PKCS12') - - - def test_empty_construction(self): - """ - :py:obj:`PKCS12` returns a new instance of :py:obj:`PKCS12` with no certificate, - private key, CA certificates, or friendly name. - """ - p12 = PKCS12() - self.assertEqual(None, p12.get_certificate()) - self.assertEqual(None, p12.get_privatekey()) - self.assertEqual(None, p12.get_ca_certificates()) - self.assertEqual(None, p12.get_friendlyname()) - - - def test_type_errors(self): - """ - The :py:obj:`PKCS12` setter functions (:py:obj:`set_certificate`, :py:obj:`set_privatekey`, - :py:obj:`set_ca_certificates`, and :py:obj:`set_friendlyname`) raise :py:obj:`TypeError` - when passed objects of types other than those expected. - """ - p12 = PKCS12() - self.assertRaises(TypeError, p12.set_certificate, 3) - self.assertRaises(TypeError, p12.set_certificate, PKey()) - self.assertRaises(TypeError, p12.set_certificate, X509) - self.assertRaises(TypeError, p12.set_privatekey, 3) - self.assertRaises(TypeError, p12.set_privatekey, 'legbone') - self.assertRaises(TypeError, p12.set_privatekey, X509()) - self.assertRaises(TypeError, p12.set_ca_certificates, 3) - self.assertRaises(TypeError, p12.set_ca_certificates, X509()) - self.assertRaises(TypeError, p12.set_ca_certificates, (3, 4)) - self.assertRaises(TypeError, p12.set_ca_certificates, ( PKey(), )) - self.assertRaises(TypeError, p12.set_friendlyname, 6) - self.assertRaises(TypeError, p12.set_friendlyname, ('foo', 'bar')) - - - def test_key_only(self): - """ - A :py:obj:`PKCS12` with only a private key can be exported using - :py:obj:`PKCS12.export` and loaded again using :py:obj:`load_pkcs12`. - """ - passwd = b"blah" - p12 = PKCS12() - pkey = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - p12.set_privatekey(pkey) - self.assertEqual(None, p12.get_certificate()) - self.assertEqual(pkey, p12.get_privatekey()) - try: - dumped_p12 = p12.export(passphrase=passwd, iter=2, maciter=3) - except Error: - # Some versions of OpenSSL will throw an exception - # for this nearly useless PKCS12 we tried to generate: - # [('PKCS12 routines', 'PKCS12_create', 'invalid null argument')] - return - p12 = load_pkcs12(dumped_p12, passwd) - self.assertEqual(None, p12.get_ca_certificates()) - self.assertEqual(None, p12.get_certificate()) - - # OpenSSL fails to bring the key back to us. So sad. Perhaps in the - # future this will be improved. - self.assertTrue(isinstance(p12.get_privatekey(), (PKey, type(None)))) - - - def test_cert_only(self): - """ - A :py:obj:`PKCS12` with only a certificate can be exported using - :py:obj:`PKCS12.export` and loaded again using :py:obj:`load_pkcs12`. - """ - passwd = b"blah" - p12 = PKCS12() - cert = load_certificate(FILETYPE_PEM, cleartextCertificatePEM) - p12.set_certificate(cert) - self.assertEqual(cert, p12.get_certificate()) - self.assertEqual(None, p12.get_privatekey()) - try: - dumped_p12 = p12.export(passphrase=passwd, iter=2, maciter=3) - except Error: - # Some versions of OpenSSL will throw an exception - # for this nearly useless PKCS12 we tried to generate: - # [('PKCS12 routines', 'PKCS12_create', 'invalid null argument')] - return - p12 = load_pkcs12(dumped_p12, passwd) - self.assertEqual(None, p12.get_privatekey()) - - # OpenSSL fails to bring the cert back to us. Groany mcgroan. - self.assertTrue(isinstance(p12.get_certificate(), (X509, type(None)))) - - # Oh ho. It puts the certificate into the ca certificates list, in - # fact. Totally bogus, I would think. Nevertheless, let's exploit - # that to check to see if it reconstructed the certificate we expected - # it to. At some point, hopefully this will change so that - # p12.get_certificate() is actually what returns the loaded - # certificate. - self.assertEqual( - cleartextCertificatePEM, - dump_certificate(FILETYPE_PEM, p12.get_ca_certificates()[0])) - - - def gen_pkcs12(self, cert_pem=None, key_pem=None, ca_pem=None, friendly_name=None): - """ - Generate a PKCS12 object with components from PEM. Verify that the set - functions return None. - """ - p12 = PKCS12() - if cert_pem: - ret = p12.set_certificate(load_certificate(FILETYPE_PEM, cert_pem)) - self.assertEqual(ret, None) - if key_pem: - ret = p12.set_privatekey(load_privatekey(FILETYPE_PEM, key_pem)) - self.assertEqual(ret, None) - if ca_pem: - ret = p12.set_ca_certificates((load_certificate(FILETYPE_PEM, ca_pem),)) - self.assertEqual(ret, None) - if friendly_name: - ret = p12.set_friendlyname(friendly_name) - self.assertEqual(ret, None) - return p12 - - - def check_recovery(self, p12_str, key=None, cert=None, ca=None, passwd=b"", - extra=()): - """ - Use openssl program to confirm three components are recoverable from a - PKCS12 string. - """ - if key: - recovered_key = _runopenssl( - p12_str, b"pkcs12", b"-nocerts", b"-nodes", b"-passin", - b"pass:" + passwd, *extra) - self.assertEqual(recovered_key[-len(key):], key) - if cert: - recovered_cert = _runopenssl( - p12_str, b"pkcs12", b"-clcerts", b"-nodes", b"-passin", - b"pass:" + passwd, b"-nokeys", *extra) - self.assertEqual(recovered_cert[-len(cert):], cert) - if ca: - recovered_cert = _runopenssl( - p12_str, b"pkcs12", b"-cacerts", b"-nodes", b"-passin", - b"pass:" + passwd, b"-nokeys", *extra) - self.assertEqual(recovered_cert[-len(ca):], ca) - - - def verify_pkcs12_container(self, p12): - """ - Verify that the PKCS#12 container contains the correct client - certificate and private key. - - :param p12: The PKCS12 instance to verify. - :type p12: :py:class:`PKCS12` - """ - cert_pem = dump_certificate(FILETYPE_PEM, p12.get_certificate()) - key_pem = dump_privatekey(FILETYPE_PEM, p12.get_privatekey()) - self.assertEqual( - (client_cert_pem, client_key_pem, None), - (cert_pem, key_pem, p12.get_ca_certificates())) - - - def test_load_pkcs12(self): - """ - A PKCS12 string generated using the openssl command line can be loaded - with :py:obj:`load_pkcs12` and its components extracted and examined. - """ - passwd = b"whatever" - pem = client_key_pem + client_cert_pem - p12_str = _runopenssl( - pem, b"pkcs12", b"-export", b"-clcerts", b"-passout", b"pass:" + passwd) - p12 = load_pkcs12(p12_str, passphrase=passwd) - self.verify_pkcs12_container(p12) - - - def test_load_pkcs12_no_passphrase(self): - """ - A PKCS12 string generated using openssl command line can be loaded with - :py:obj:`load_pkcs12` without a passphrase and its components extracted - and examined. - """ - pem = client_key_pem + client_cert_pem - p12_str = _runopenssl( - pem, b"pkcs12", b"-export", b"-clcerts", b"-passout", b"pass:") - p12 = load_pkcs12(p12_str) - self.verify_pkcs12_container(p12) - - - def _dump_and_load(self, dump_passphrase, load_passphrase): - """ - A helper method to dump and load a PKCS12 object. - """ - p12 = self.gen_pkcs12(client_cert_pem, client_key_pem) - dumped_p12 = p12.export(passphrase=dump_passphrase, iter=2, maciter=3) - return load_pkcs12(dumped_p12, passphrase=load_passphrase) - - - def test_load_pkcs12_null_passphrase_load_empty(self): - """ - A PKCS12 string can be dumped with a null passphrase, loaded with an - empty passphrase with :py:obj:`load_pkcs12`, and its components - extracted and examined. - """ - self.verify_pkcs12_container( - self._dump_and_load(dump_passphrase=None, load_passphrase=b'')) - - - def test_load_pkcs12_null_passphrase_load_null(self): - """ - A PKCS12 string can be dumped with a null passphrase, loaded with a - null passphrase with :py:obj:`load_pkcs12`, and its components - extracted and examined. - """ - self.verify_pkcs12_container( - self._dump_and_load(dump_passphrase=None, load_passphrase=None)) - - - def test_load_pkcs12_empty_passphrase_load_empty(self): - """ - A PKCS12 string can be dumped with an empty passphrase, loaded with an - empty passphrase with :py:obj:`load_pkcs12`, and its components - extracted and examined. - """ - self.verify_pkcs12_container( - self._dump_and_load(dump_passphrase=b'', load_passphrase=b'')) - - - def test_load_pkcs12_empty_passphrase_load_null(self): - """ - A PKCS12 string can be dumped with an empty passphrase, loaded with a - null passphrase with :py:obj:`load_pkcs12`, and its components - extracted and examined. - """ - self.verify_pkcs12_container( - self._dump_and_load(dump_passphrase=b'', load_passphrase=None)) - - - def test_load_pkcs12_garbage(self): - """ - :py:obj:`load_pkcs12` raises :py:obj:`OpenSSL.crypto.Error` when passed a string - which is not a PKCS12 dump. - """ - passwd = 'whatever' - e = self.assertRaises(Error, load_pkcs12, b'fruit loops', passwd) - self.assertEqual( e.args[0][0][0], 'asn1 encoding routines') - self.assertEqual( len(e.args[0][0]), 3) - - - def test_replace(self): - """ - :py:obj:`PKCS12.set_certificate` replaces the certificate in a PKCS12 cluster. - :py:obj:`PKCS12.set_privatekey` replaces the private key. - :py:obj:`PKCS12.set_ca_certificates` replaces the CA certificates. - """ - p12 = self.gen_pkcs12(client_cert_pem, client_key_pem, root_cert_pem) - p12.set_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) - p12.set_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) - root_cert = load_certificate(FILETYPE_PEM, root_cert_pem) - client_cert = load_certificate(FILETYPE_PEM, client_cert_pem) - p12.set_ca_certificates([root_cert]) # not a tuple - self.assertEqual(1, len(p12.get_ca_certificates())) - self.assertEqual(root_cert, p12.get_ca_certificates()[0]) - p12.set_ca_certificates([client_cert, root_cert]) - self.assertEqual(2, len(p12.get_ca_certificates())) - self.assertEqual(client_cert, p12.get_ca_certificates()[0]) - self.assertEqual(root_cert, p12.get_ca_certificates()[1]) - - - def test_friendly_name(self): - """ - The *friendlyName* of a PKCS12 can be set and retrieved via - :py:obj:`PKCS12.get_friendlyname` and :py:obj:`PKCS12_set_friendlyname`, and a - :py:obj:`PKCS12` with a friendly name set can be dumped with :py:obj:`PKCS12.export`. - """ - passwd = b'Dogmeat[]{}!@#$%^&*()~`?/.,<>-_+=";:' - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - for friendly_name in [b('Serverlicious'), None, b('###')]: - p12.set_friendlyname(friendly_name) - self.assertEqual(p12.get_friendlyname(), friendly_name) - dumped_p12 = p12.export(passphrase=passwd, iter=2, maciter=3) - reloaded_p12 = load_pkcs12(dumped_p12, passwd) - self.assertEqual( - p12.get_friendlyname(), reloaded_p12.get_friendlyname()) - # We would use the openssl program to confirm the friendly - # name, but it is not possible. The pkcs12 command - # does not store the friendly name in the cert's - # alias, which we could then extract. - self.check_recovery( - dumped_p12, key=server_key_pem, cert=server_cert_pem, - ca=root_cert_pem, passwd=passwd) - - - def test_various_empty_passphrases(self): - """ - Test that missing, None, and '' passphrases are identical for PKCS12 - export. - """ - p12 = self.gen_pkcs12(client_cert_pem, client_key_pem, root_cert_pem) - passwd = b"" - dumped_p12_empty = p12.export(iter=2, maciter=0, passphrase=passwd) - dumped_p12_none = p12.export(iter=3, maciter=2, passphrase=None) - dumped_p12_nopw = p12.export(iter=9, maciter=4) - for dumped_p12 in [dumped_p12_empty, dumped_p12_none, dumped_p12_nopw]: - self.check_recovery( - dumped_p12, key=client_key_pem, cert=client_cert_pem, - ca=root_cert_pem, passwd=passwd) - - - def test_removing_ca_cert(self): - """ - Passing :py:obj:`None` to :py:obj:`PKCS12.set_ca_certificates` removes all CA - certificates. - """ - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - p12.set_ca_certificates(None) - self.assertEqual(None, p12.get_ca_certificates()) - - - def test_export_without_mac(self): - """ - Exporting a PKCS12 with a :py:obj:`maciter` of ``-1`` excludes the MAC - entirely. - """ - passwd = b"Lake Michigan" - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - dumped_p12 = p12.export(maciter=-1, passphrase=passwd, iter=2) - self.check_recovery( - dumped_p12, key=server_key_pem, cert=server_cert_pem, - passwd=passwd, extra=(b"-nomacver",)) - - - def test_load_without_mac(self): - """ - Loading a PKCS12 without a MAC does something other than crash. - """ - passwd = b"Lake Michigan" - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - dumped_p12 = p12.export(maciter=-1, passphrase=passwd, iter=2) - try: - recovered_p12 = load_pkcs12(dumped_p12, passwd) - # The person who generated this PCKS12 should be flogged, - # or better yet we should have a means to determine - # whether a PCKS12 had a MAC that was verified. - # Anyway, libopenssl chooses to allow it, so the - # pyopenssl binding does as well. - self.assertTrue(isinstance(recovered_p12, PKCS12)) - except Error: - # Failing here with an exception is preferred as some openssl - # versions do. - pass - - - def test_zero_len_list_for_ca(self): - """ - A PKCS12 with an empty CA certificates list can be exported. - """ - passwd = 'Hobie 18' - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem) - # p12.set_ca_certificates([]) - # self.assertEqual((), p12.get_ca_certificates()) - # dumped_p12 = p12.export(passphrase=passwd, iter=3) - # self.check_recovery( - # dumped_p12, key=server_key_pem, cert=server_cert_pem, - # passwd=passwd) - - - def test_export_without_args(self): - """ - All the arguments to :py:obj:`PKCS12.export` are optional. - """ - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - dumped_p12 = p12.export() # no args - self.check_recovery( - dumped_p12, key=server_key_pem, cert=server_cert_pem, passwd=b"") - - - def test_key_cert_mismatch(self): - """ - :py:obj:`PKCS12.export` raises an exception when a key and certificate - mismatch. - """ - p12 = self.gen_pkcs12(server_cert_pem, client_key_pem, root_cert_pem) - self.assertRaises(Error, p12.export) - - - -# These quoting functions taken directly from Twisted's twisted.python.win32. -_cmdLineQuoteRe = re.compile(br'(\\*)"') -_cmdLineQuoteRe2 = re.compile(br'(\\+)\Z') -def cmdLineQuote(s): - """ - Internal method for quoting a single command-line argument. - - See http://www.perlmonks.org/?node_id=764004 - - :type: :py:obj:`str` - :param s: A single unquoted string to quote for something that is expecting - cmd.exe-style quoting - - :rtype: :py:obj:`str` - :return: A cmd.exe-style quoted string - """ - s = _cmdLineQuoteRe2.sub(br"\1\1", _cmdLineQuoteRe.sub(br'\1\1\\"', s)) - return b'"' + s + b'"' - - - -def quoteArguments(arguments): - """ - Quote an iterable of command-line arguments for passing to CreateProcess or - a similar API. This allows the list passed to :py:obj:`reactor.spawnProcess` to - match the child process's :py:obj:`sys.argv` properly. - - :type arguments: :py:obj:`iterable` of :py:obj:`str` - :param arguments: An iterable of unquoted arguments to quote - - :rtype: :py:obj:`str` - :return: A space-delimited string containing quoted versions of :py:obj:`arguments` - """ - return b' '.join(map(cmdLineQuote, arguments)) - - - -def _runopenssl(pem, *args): - """ - Run the command line openssl tool with the given arguments and write - the given PEM to its stdin. Not safe for quotes. - """ - if os.name == 'posix': - command = b"openssl " + b" ".join([ - (b"'" + arg.replace(b"'", b"'\\''") + b"'") - for arg in args]) - else: - command = b"openssl " + quoteArguments(args) - proc = Popen(native(command), shell=True, stdin=PIPE, stdout=PIPE) - proc.stdin.write(pem) - proc.stdin.close() - output = proc.stdout.read() - proc.stdout.close() - proc.wait() - return output - - - -class FunctionTests(TestCase): - """ - Tests for free-functions in the :py:obj:`OpenSSL.crypto` module. - """ - - def test_load_privatekey_invalid_format(self): - """ - :py:obj:`load_privatekey` raises :py:obj:`ValueError` if passed an unknown filetype. - """ - self.assertRaises(ValueError, load_privatekey, 100, root_key_pem) - - - def test_load_privatekey_invalid_passphrase_type(self): - """ - :py:obj:`load_privatekey` raises :py:obj:`TypeError` if passed a passphrase that is - neither a :py:obj:`str` nor a callable. - """ - self.assertRaises( - TypeError, - load_privatekey, - FILETYPE_PEM, encryptedPrivateKeyPEMPassphrase, object()) - - - def test_load_privatekey_wrong_args(self): - """ - :py:obj:`load_privatekey` raises :py:obj:`TypeError` if called with the wrong number - of arguments. - """ - self.assertRaises(TypeError, load_privatekey) - - - def test_load_privatekey_wrongPassphrase(self): - """ - :py:obj:`load_privatekey` raises :py:obj:`OpenSSL.crypto.Error` when it is passed an - encrypted PEM and an incorrect passphrase. - """ - self.assertRaises( - Error, - load_privatekey, FILETYPE_PEM, encryptedPrivateKeyPEM, b("quack")) - - - def test_load_privatekey_passphraseWrongType(self): - """ - :py:obj:`load_privatekey` raises :py:obj:`ValueError` when it is passed a passphrase - with a private key encoded in a format, that doesn't support - encryption. - """ - key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - blob = dump_privatekey(FILETYPE_ASN1, key) - self.assertRaises(ValueError, - load_privatekey, FILETYPE_ASN1, blob, "secret") - - - def test_load_privatekey_passphrase(self): - """ - :py:obj:`load_privatekey` can create a :py:obj:`PKey` object from an encrypted PEM - string if given the passphrase. - """ - key = load_privatekey( - FILETYPE_PEM, encryptedPrivateKeyPEM, - encryptedPrivateKeyPEMPassphrase) - self.assertTrue(isinstance(key, PKeyType)) - - - def test_load_privatekey_passphrase_exception(self): - """ - If the passphrase callback raises an exception, that exception is raised - by :py:obj:`load_privatekey`. - """ - def cb(ignored): - raise ArithmeticError - - self.assertRaises(ArithmeticError, - load_privatekey, FILETYPE_PEM, encryptedPrivateKeyPEM, cb) - - - def test_load_privatekey_wrongPassphraseCallback(self): - """ - :py:obj:`load_privatekey` raises :py:obj:`OpenSSL.crypto.Error` when it - is passed an encrypted PEM and a passphrase callback which returns an - incorrect passphrase. - """ - called = [] - def cb(*a): - called.append(None) - return b("quack") - self.assertRaises( - Error, - load_privatekey, FILETYPE_PEM, encryptedPrivateKeyPEM, cb) - self.assertTrue(called) - - - def test_load_privatekey_passphraseCallback(self): - """ - :py:obj:`load_privatekey` can create a :py:obj:`PKey` object from an encrypted PEM - string if given a passphrase callback which returns the correct - password. - """ - called = [] - def cb(writing): - called.append(writing) - return encryptedPrivateKeyPEMPassphrase - key = load_privatekey(FILETYPE_PEM, encryptedPrivateKeyPEM, cb) - self.assertTrue(isinstance(key, PKeyType)) - self.assertEqual(called, [False]) - - - def test_load_privatekey_passphrase_wrong_return_type(self): - """ - :py:obj:`load_privatekey` raises :py:obj:`ValueError` if the passphrase - callback returns something other than a byte string. - """ - self.assertRaises( - ValueError, - load_privatekey, - FILETYPE_PEM, encryptedPrivateKeyPEM, lambda *args: 3) - - - def test_dump_privatekey_wrong_args(self): - """ - :py:obj:`dump_privatekey` raises :py:obj:`TypeError` if called with the wrong number - of arguments. - """ - self.assertRaises(TypeError, dump_privatekey) - # If cipher name is given, password is required. - self.assertRaises( - TypeError, dump_privatekey, FILETYPE_PEM, PKey(), GOOD_CIPHER) - - - def test_dump_privatekey_unknown_cipher(self): - """ - :py:obj:`dump_privatekey` raises :py:obj:`ValueError` if called with an unrecognized - cipher name. - """ - key = PKey() - key.generate_key(TYPE_RSA, 512) - self.assertRaises( - ValueError, dump_privatekey, - FILETYPE_PEM, key, BAD_CIPHER, "passphrase") - - - def test_dump_privatekey_invalid_passphrase_type(self): - """ - :py:obj:`dump_privatekey` raises :py:obj:`TypeError` if called with a passphrase which - is neither a :py:obj:`str` nor a callable. - """ - key = PKey() - key.generate_key(TYPE_RSA, 512) - self.assertRaises( - TypeError, - dump_privatekey, FILETYPE_PEM, key, GOOD_CIPHER, object()) - - - def test_dump_privatekey_invalid_filetype(self): - """ - :py:obj:`dump_privatekey` raises :py:obj:`ValueError` if called with an unrecognized - filetype. - """ - key = PKey() - key.generate_key(TYPE_RSA, 512) - self.assertRaises(ValueError, dump_privatekey, 100, key) - - - def test_load_privatekey_passphraseCallbackLength(self): - """ - :py:obj:`crypto.load_privatekey` should raise an error when the passphrase - provided by the callback is too long, not silently truncate it. - """ - def cb(ignored): - return "a" * 1025 - - self.assertRaises(ValueError, - load_privatekey, FILETYPE_PEM, encryptedPrivateKeyPEM, cb) - - - def test_dump_privatekey_passphrase(self): - """ - :py:obj:`dump_privatekey` writes an encrypted PEM when given a passphrase. - """ - passphrase = b("foo") - key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - pem = dump_privatekey(FILETYPE_PEM, key, GOOD_CIPHER, passphrase) - self.assertTrue(isinstance(pem, binary_type)) - loadedKey = load_privatekey(FILETYPE_PEM, pem, passphrase) - self.assertTrue(isinstance(loadedKey, PKeyType)) - self.assertEqual(loadedKey.type(), key.type()) - self.assertEqual(loadedKey.bits(), key.bits()) - - - def test_dump_privatekey_passphraseWrongType(self): - """ - :py:obj:`dump_privatekey` raises :py:obj:`ValueError` when it is passed a passphrase - with a private key encoded in a format, that doesn't support - encryption. - """ - key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - self.assertRaises(ValueError, - dump_privatekey, FILETYPE_ASN1, key, GOOD_CIPHER, "secret") - - - def test_dump_certificate(self): - """ - :py:obj:`dump_certificate` writes PEM, DER, and text. - """ - pemData = cleartextCertificatePEM + cleartextPrivateKeyPEM - cert = load_certificate(FILETYPE_PEM, pemData) - dumped_pem = dump_certificate(FILETYPE_PEM, cert) - self.assertEqual(dumped_pem, cleartextCertificatePEM) - dumped_der = dump_certificate(FILETYPE_ASN1, cert) - good_der = _runopenssl(dumped_pem, b"x509", b"-outform", b"DER") - self.assertEqual(dumped_der, good_der) - cert2 = load_certificate(FILETYPE_ASN1, dumped_der) - dumped_pem2 = dump_certificate(FILETYPE_PEM, cert2) - self.assertEqual(dumped_pem2, cleartextCertificatePEM) - dumped_text = dump_certificate(FILETYPE_TEXT, cert) - good_text = _runopenssl(dumped_pem, b"x509", b"-noout", b"-text") - self.assertEqual(dumped_text, good_text) - - - def test_dump_privatekey_pem(self): - """ - :py:obj:`dump_privatekey` writes a PEM - """ - key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - self.assertTrue(key.check()) - dumped_pem = dump_privatekey(FILETYPE_PEM, key) - self.assertEqual(dumped_pem, cleartextPrivateKeyPEM) - - - def test_dump_privatekey_asn1(self): - """ - :py:obj:`dump_privatekey` writes a DER - """ - key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - dumped_pem = dump_privatekey(FILETYPE_PEM, key) - - dumped_der = dump_privatekey(FILETYPE_ASN1, key) - # XXX This OpenSSL call writes "writing RSA key" to standard out. Sad. - good_der = _runopenssl(dumped_pem, b"rsa", b"-outform", b"DER") - self.assertEqual(dumped_der, good_der) - key2 = load_privatekey(FILETYPE_ASN1, dumped_der) - dumped_pem2 = dump_privatekey(FILETYPE_PEM, key2) - self.assertEqual(dumped_pem2, cleartextPrivateKeyPEM) - - - def test_dump_privatekey_text(self): - """ - :py:obj:`dump_privatekey` writes a text - """ - key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - dumped_pem = dump_privatekey(FILETYPE_PEM, key) - - dumped_text = dump_privatekey(FILETYPE_TEXT, key) - good_text = _runopenssl(dumped_pem, b"rsa", b"-noout", b"-text") - self.assertEqual(dumped_text, good_text) - - - def test_dump_certificate_request(self): - """ - :py:obj:`dump_certificate_request` writes a PEM, DER, and text. - """ - req = load_certificate_request(FILETYPE_PEM, cleartextCertificateRequestPEM) - dumped_pem = dump_certificate_request(FILETYPE_PEM, req) - self.assertEqual(dumped_pem, cleartextCertificateRequestPEM) - dumped_der = dump_certificate_request(FILETYPE_ASN1, req) - good_der = _runopenssl(dumped_pem, b"req", b"-outform", b"DER") - self.assertEqual(dumped_der, good_der) - req2 = load_certificate_request(FILETYPE_ASN1, dumped_der) - dumped_pem2 = dump_certificate_request(FILETYPE_PEM, req2) - self.assertEqual(dumped_pem2, cleartextCertificateRequestPEM) - dumped_text = dump_certificate_request(FILETYPE_TEXT, req) - good_text = _runopenssl(dumped_pem, b"req", b"-noout", b"-text") - self.assertEqual(dumped_text, good_text) - self.assertRaises(ValueError, dump_certificate_request, 100, req) - - - def test_dump_privatekey_passphraseCallback(self): - """ - :py:obj:`dump_privatekey` writes an encrypted PEM when given a callback which - returns the correct passphrase. - """ - passphrase = b("foo") - called = [] - def cb(writing): - called.append(writing) - return passphrase - key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - pem = dump_privatekey(FILETYPE_PEM, key, GOOD_CIPHER, cb) - self.assertTrue(isinstance(pem, binary_type)) - self.assertEqual(called, [True]) - loadedKey = load_privatekey(FILETYPE_PEM, pem, passphrase) - self.assertTrue(isinstance(loadedKey, PKeyType)) - self.assertEqual(loadedKey.type(), key.type()) - self.assertEqual(loadedKey.bits(), key.bits()) - - - def test_dump_privatekey_passphrase_exception(self): - """ - :py:obj:`dump_privatekey` should not overwrite the exception raised - by the passphrase callback. - """ - def cb(ignored): - raise ArithmeticError - - key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - self.assertRaises(ArithmeticError, - dump_privatekey, FILETYPE_PEM, key, GOOD_CIPHER, cb) - - - def test_dump_privatekey_passphraseCallbackLength(self): - """ - :py:obj:`crypto.dump_privatekey` should raise an error when the passphrase - provided by the callback is too long, not silently truncate it. - """ - def cb(ignored): - return "a" * 1025 - - key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - self.assertRaises(ValueError, - dump_privatekey, FILETYPE_PEM, key, GOOD_CIPHER, cb) - - - def test_load_pkcs7_data(self): - """ - :py:obj:`load_pkcs7_data` accepts a PKCS#7 string and returns an instance of - :py:obj:`PKCS7Type`. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertTrue(isinstance(pkcs7, PKCS7Type)) - - - def test_load_pkcs7_data_invalid(self): - """ - If the data passed to :py:obj:`load_pkcs7_data` is invalid, - :py:obj:`Error` is raised. - """ - self.assertRaises(Error, load_pkcs7_data, FILETYPE_PEM, b"foo") - - - -class LoadCertificateTests(TestCase): - """ - Tests for :py:obj:`load_certificate_request`. - """ - def test_badFileType(self): - """ - If the file type passed to :py:obj:`load_certificate_request` is - neither :py:obj:`FILETYPE_PEM` nor :py:obj:`FILETYPE_ASN1` then - :py:class:`ValueError` is raised. - """ - self.assertRaises(ValueError, load_certificate_request, object(), b"") - - - -class PKCS7Tests(TestCase): - """ - Tests for :py:obj:`PKCS7Type`. - """ - def test_type(self): - """ - :py:obj:`PKCS7Type` is a type object. - """ - self.assertTrue(isinstance(PKCS7Type, type)) - self.assertEqual(PKCS7Type.__name__, 'PKCS7') - - # XXX This doesn't currently work. - # self.assertIdentical(PKCS7, PKCS7Type) - - - # XXX Opposite results for all these following methods - - def test_type_is_signed_wrong_args(self): - """ - :py:obj:`PKCS7Type.type_is_signed` raises :py:obj:`TypeError` if called with any - arguments. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertRaises(TypeError, pkcs7.type_is_signed, None) - - - def test_type_is_signed(self): - """ - :py:obj:`PKCS7Type.type_is_signed` returns :py:obj:`True` if the PKCS7 object is of - the type *signed*. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertTrue(pkcs7.type_is_signed()) - - - def test_type_is_enveloped_wrong_args(self): - """ - :py:obj:`PKCS7Type.type_is_enveloped` raises :py:obj:`TypeError` if called with any - arguments. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertRaises(TypeError, pkcs7.type_is_enveloped, None) - - - def test_type_is_enveloped(self): - """ - :py:obj:`PKCS7Type.type_is_enveloped` returns :py:obj:`False` if the PKCS7 object is - not of the type *enveloped*. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertFalse(pkcs7.type_is_enveloped()) - - - def test_type_is_signedAndEnveloped_wrong_args(self): - """ - :py:obj:`PKCS7Type.type_is_signedAndEnveloped` raises :py:obj:`TypeError` if called - with any arguments. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertRaises(TypeError, pkcs7.type_is_signedAndEnveloped, None) - - - def test_type_is_signedAndEnveloped(self): - """ - :py:obj:`PKCS7Type.type_is_signedAndEnveloped` returns :py:obj:`False` if the PKCS7 - object is not of the type *signed and enveloped*. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertFalse(pkcs7.type_is_signedAndEnveloped()) - - - def test_type_is_data(self): - """ - :py:obj:`PKCS7Type.type_is_data` returns :py:obj:`False` if the PKCS7 object is not of - the type data. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertFalse(pkcs7.type_is_data()) - - - def test_type_is_data_wrong_args(self): - """ - :py:obj:`PKCS7Type.type_is_data` raises :py:obj:`TypeError` if called with any - arguments. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertRaises(TypeError, pkcs7.type_is_data, None) - - - def test_get_type_name_wrong_args(self): - """ - :py:obj:`PKCS7Type.get_type_name` raises :py:obj:`TypeError` if called with any - arguments. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertRaises(TypeError, pkcs7.get_type_name, None) - - - def test_get_type_name(self): - """ - :py:obj:`PKCS7Type.get_type_name` returns a :py:obj:`str` giving the type name. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertEquals(pkcs7.get_type_name(), b('pkcs7-signedData')) - - - def test_attribute(self): - """ - If an attribute other than one of the methods tested here is accessed on - an instance of :py:obj:`PKCS7Type`, :py:obj:`AttributeError` is raised. - """ - pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data) - self.assertRaises(AttributeError, getattr, pkcs7, "foo") - - - -class NetscapeSPKITests(TestCase, _PKeyInteractionTestsMixin): - """ - Tests for :py:obj:`OpenSSL.crypto.NetscapeSPKI`. - """ - def signable(self): - """ - Return a new :py:obj:`NetscapeSPKI` for use with signing tests. - """ - return NetscapeSPKI() - - - def test_type(self): - """ - :py:obj:`NetscapeSPKI` and :py:obj:`NetscapeSPKIType` refer to the same type object - and can be used to create instances of that type. - """ - self.assertIdentical(NetscapeSPKI, NetscapeSPKIType) - self.assertConsistentType(NetscapeSPKI, 'NetscapeSPKI') - - - def test_construction(self): - """ - :py:obj:`NetscapeSPKI` returns an instance of :py:obj:`NetscapeSPKIType`. - """ - nspki = NetscapeSPKI() - self.assertTrue(isinstance(nspki, NetscapeSPKIType)) - - - def test_invalid_attribute(self): - """ - Accessing a non-existent attribute of a :py:obj:`NetscapeSPKI` instance causes - an :py:obj:`AttributeError` to be raised. - """ - nspki = NetscapeSPKI() - self.assertRaises(AttributeError, lambda: nspki.foo) - - - def test_b64_encode(self): - """ - :py:obj:`NetscapeSPKI.b64_encode` encodes the certificate to a base64 blob. - """ - nspki = NetscapeSPKI() - blob = nspki.b64_encode() - self.assertTrue(isinstance(blob, binary_type)) - - - -class RevokedTests(TestCase): - """ - Tests for :py:obj:`OpenSSL.crypto.Revoked` - """ - def test_construction(self): - """ - Confirm we can create :py:obj:`OpenSSL.crypto.Revoked`. Check - that it is empty. - """ - revoked = Revoked() - self.assertTrue(isinstance(revoked, Revoked)) - self.assertEquals(type(revoked), Revoked) - self.assertEquals(revoked.get_serial(), b('00')) - self.assertEquals(revoked.get_rev_date(), None) - self.assertEquals(revoked.get_reason(), None) - - - def test_construction_wrong_args(self): - """ - Calling :py:obj:`OpenSSL.crypto.Revoked` with any arguments results - in a :py:obj:`TypeError` being raised. - """ - self.assertRaises(TypeError, Revoked, None) - self.assertRaises(TypeError, Revoked, 1) - self.assertRaises(TypeError, Revoked, "foo") - - - def test_serial(self): - """ - Confirm we can set and get serial numbers from - :py:obj:`OpenSSL.crypto.Revoked`. Confirm errors are handled - with grace. - """ - revoked = Revoked() - ret = revoked.set_serial(b('10b')) - self.assertEquals(ret, None) - ser = revoked.get_serial() - self.assertEquals(ser, b('010B')) - - revoked.set_serial(b('31ppp')) # a type error would be nice - ser = revoked.get_serial() - self.assertEquals(ser, b('31')) - - self.assertRaises(ValueError, revoked.set_serial, b('pqrst')) - self.assertRaises(TypeError, revoked.set_serial, 100) - self.assertRaises(TypeError, revoked.get_serial, 1) - self.assertRaises(TypeError, revoked.get_serial, None) - self.assertRaises(TypeError, revoked.get_serial, "") - - - def test_date(self): - """ - Confirm we can set and get revocation dates from - :py:obj:`OpenSSL.crypto.Revoked`. Confirm errors are handled - with grace. - """ - revoked = Revoked() - date = revoked.get_rev_date() - self.assertEquals(date, None) - - now = b(datetime.now().strftime("%Y%m%d%H%M%SZ")) - ret = revoked.set_rev_date(now) - self.assertEqual(ret, None) - date = revoked.get_rev_date() - self.assertEqual(date, now) - - - def test_reason(self): - """ - Confirm we can set and get revocation reasons from - :py:obj:`OpenSSL.crypto.Revoked`. The "get" need to work - as "set". Likewise, each reason of all_reasons() must work. - """ - revoked = Revoked() - for r in revoked.all_reasons(): - for x in range(2): - ret = revoked.set_reason(r) - self.assertEquals(ret, None) - reason = revoked.get_reason() - self.assertEquals( - reason.lower().replace(b(' '), b('')), - r.lower().replace(b(' '), b(''))) - r = reason # again with the resp of get - - revoked.set_reason(None) - self.assertEqual(revoked.get_reason(), None) - - - def test_set_reason_wrong_arguments(self): - """ - Calling :py:obj:`OpenSSL.crypto.Revoked.set_reason` with other than - one argument, or an argument which isn't a valid reason, - results in :py:obj:`TypeError` or :py:obj:`ValueError` being raised. - """ - revoked = Revoked() - self.assertRaises(TypeError, revoked.set_reason, 100) - self.assertRaises(ValueError, revoked.set_reason, b('blue')) - - - def test_get_reason_wrong_arguments(self): - """ - Calling :py:obj:`OpenSSL.crypto.Revoked.get_reason` with any - arguments results in :py:obj:`TypeError` being raised. - """ - revoked = Revoked() - self.assertRaises(TypeError, revoked.get_reason, None) - self.assertRaises(TypeError, revoked.get_reason, 1) - self.assertRaises(TypeError, revoked.get_reason, "foo") - - - -class CRLTests(TestCase): - """ - Tests for :py:obj:`OpenSSL.crypto.CRL` - """ - cert = load_certificate(FILETYPE_PEM, cleartextCertificatePEM) - pkey = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) - - def test_construction(self): - """ - Confirm we can create :py:obj:`OpenSSL.crypto.CRL`. Check - that it is empty - """ - crl = CRL() - self.assertTrue( isinstance(crl, CRL) ) - self.assertEqual(crl.get_revoked(), None) - - - def test_construction_wrong_args(self): - """ - Calling :py:obj:`OpenSSL.crypto.CRL` with any number of arguments - results in a :py:obj:`TypeError` being raised. - """ - self.assertRaises(TypeError, CRL, 1) - self.assertRaises(TypeError, CRL, "") - self.assertRaises(TypeError, CRL, None) - - - def test_export(self): - """ - Use python to create a simple CRL with a revocation, and export - the CRL in formats of PEM, DER and text. Those outputs are verified - with the openssl program. - """ - crl = CRL() - revoked = Revoked() - now = b(datetime.now().strftime("%Y%m%d%H%M%SZ")) - revoked.set_rev_date(now) - revoked.set_serial(b('3ab')) - revoked.set_reason(b('sUpErSeDEd')) - crl.add_revoked(revoked) - - # PEM format - dumped_crl = crl.export(self.cert, self.pkey, days=20) - text = _runopenssl(dumped_crl, b"crl", b"-noout", b"-text") - text.index(b('Serial Number: 03AB')) - text.index(b('Superseded')) - text.index(b('Issuer: /C=US/ST=IL/L=Chicago/O=Testing/CN=Testing Root CA')) - - # DER format - dumped_crl = crl.export(self.cert, self.pkey, FILETYPE_ASN1) - text = _runopenssl(dumped_crl, b"crl", b"-noout", b"-text", b"-inform", b"DER") - text.index(b('Serial Number: 03AB')) - text.index(b('Superseded')) - text.index(b('Issuer: /C=US/ST=IL/L=Chicago/O=Testing/CN=Testing Root CA')) - - # text format - dumped_text = crl.export(self.cert, self.pkey, type=FILETYPE_TEXT) - self.assertEqual(text, dumped_text) - - - def test_export_invalid(self): - """ - If :py:obj:`CRL.export` is used with an uninitialized :py:obj:`X509` - instance, :py:obj:`OpenSSL.crypto.Error` is raised. - """ - crl = CRL() - self.assertRaises(Error, crl.export, X509(), PKey()) - - - def test_add_revoked_keyword(self): - """ - :py:obj:`OpenSSL.CRL.add_revoked` accepts its single argument as the - ``revoked`` keyword argument. - """ - crl = CRL() - revoked = Revoked() - crl.add_revoked(revoked=revoked) - self.assertTrue(isinstance(crl.get_revoked()[0], Revoked)) - - - def test_export_wrong_args(self): - """ - Calling :py:obj:`OpenSSL.CRL.export` with fewer than two or more than - four arguments, or with arguments other than the certificate, - private key, integer file type, and integer number of days it - expects, results in a :py:obj:`TypeError` being raised. - """ - crl = CRL() - self.assertRaises(TypeError, crl.export) - self.assertRaises(TypeError, crl.export, self.cert) - self.assertRaises(TypeError, crl.export, self.cert, self.pkey, FILETYPE_PEM, 10, "foo") - - self.assertRaises(TypeError, crl.export, None, self.pkey, FILETYPE_PEM, 10) - self.assertRaises(TypeError, crl.export, self.cert, None, FILETYPE_PEM, 10) - self.assertRaises(TypeError, crl.export, self.cert, self.pkey, None, 10) - self.assertRaises(TypeError, crl.export, self.cert, FILETYPE_PEM, None) - - - def test_export_unknown_filetype(self): - """ - Calling :py:obj:`OpenSSL.CRL.export` with a file type other than - :py:obj:`FILETYPE_PEM`, :py:obj:`FILETYPE_ASN1`, or :py:obj:`FILETYPE_TEXT` results - in a :py:obj:`ValueError` being raised. - """ - crl = CRL() - self.assertRaises(ValueError, crl.export, self.cert, self.pkey, 100, 10) - - - def test_get_revoked(self): - """ - Use python to create a simple CRL with two revocations. - Get back the :py:obj:`Revoked` using :py:obj:`OpenSSL.CRL.get_revoked` and - verify them. - """ - crl = CRL() - - revoked = Revoked() - now = b(datetime.now().strftime("%Y%m%d%H%M%SZ")) - revoked.set_rev_date(now) - revoked.set_serial(b('3ab')) - crl.add_revoked(revoked) - revoked.set_serial(b('100')) - revoked.set_reason(b('sUpErSeDEd')) - crl.add_revoked(revoked) - - revs = crl.get_revoked() - self.assertEqual(len(revs), 2) - self.assertEqual(type(revs[0]), Revoked) - self.assertEqual(type(revs[1]), Revoked) - self.assertEqual(revs[0].get_serial(), b('03AB')) - self.assertEqual(revs[1].get_serial(), b('0100')) - self.assertEqual(revs[0].get_rev_date(), now) - self.assertEqual(revs[1].get_rev_date(), now) - - - def test_get_revoked_wrong_args(self): - """ - Calling :py:obj:`OpenSSL.CRL.get_revoked` with any arguments results - in a :py:obj:`TypeError` being raised. - """ - crl = CRL() - self.assertRaises(TypeError, crl.get_revoked, None) - self.assertRaises(TypeError, crl.get_revoked, 1) - self.assertRaises(TypeError, crl.get_revoked, "") - self.assertRaises(TypeError, crl.get_revoked, "", 1, None) - - - def test_add_revoked_wrong_args(self): - """ - Calling :py:obj:`OpenSSL.CRL.add_revoked` with other than one - argument results in a :py:obj:`TypeError` being raised. - """ - crl = CRL() - self.assertRaises(TypeError, crl.add_revoked) - self.assertRaises(TypeError, crl.add_revoked, 1, 2) - self.assertRaises(TypeError, crl.add_revoked, "foo", "bar") - - - def test_load_crl(self): - """ - Load a known CRL and inspect its revocations. Both - PEM and DER formats are loaded. - """ - crl = load_crl(FILETYPE_PEM, crlData) - revs = crl.get_revoked() - self.assertEqual(len(revs), 2) - self.assertEqual(revs[0].get_serial(), b('03AB')) - self.assertEqual(revs[0].get_reason(), None) - self.assertEqual(revs[1].get_serial(), b('0100')) - self.assertEqual(revs[1].get_reason(), b('Superseded')) - - der = _runopenssl(crlData, b"crl", b"-outform", b"DER") - crl = load_crl(FILETYPE_ASN1, der) - revs = crl.get_revoked() - self.assertEqual(len(revs), 2) - self.assertEqual(revs[0].get_serial(), b('03AB')) - self.assertEqual(revs[0].get_reason(), None) - self.assertEqual(revs[1].get_serial(), b('0100')) - self.assertEqual(revs[1].get_reason(), b('Superseded')) - - - def test_load_crl_wrong_args(self): - """ - Calling :py:obj:`OpenSSL.crypto.load_crl` with other than two - arguments results in a :py:obj:`TypeError` being raised. - """ - self.assertRaises(TypeError, load_crl) - self.assertRaises(TypeError, load_crl, FILETYPE_PEM) - self.assertRaises(TypeError, load_crl, FILETYPE_PEM, crlData, None) - - - def test_load_crl_bad_filetype(self): - """ - Calling :py:obj:`OpenSSL.crypto.load_crl` with an unknown file type - raises a :py:obj:`ValueError`. - """ - self.assertRaises(ValueError, load_crl, 100, crlData) - - - def test_load_crl_bad_data(self): - """ - Calling :py:obj:`OpenSSL.crypto.load_crl` with file data which can't - be loaded raises a :py:obj:`OpenSSL.crypto.Error`. - """ - self.assertRaises(Error, load_crl, FILETYPE_PEM, b"hello, world") - - - -class SignVerifyTests(TestCase): - """ - Tests for :py:obj:`OpenSSL.crypto.sign` and :py:obj:`OpenSSL.crypto.verify`. - """ - def test_sign_verify(self): - """ - :py:obj:`sign` generates a cryptographic signature which :py:obj:`verify` can check. - """ - content = b( - "It was a bright cold day in April, and the clocks were striking " - "thirteen. Winston Smith, his chin nuzzled into his breast in an " - "effort to escape the vile wind, slipped quickly through the " - "glass doors of Victory Mansions, though not quickly enough to " - "prevent a swirl of gritty dust from entering along with him.") - - # sign the content with this private key - priv_key = load_privatekey(FILETYPE_PEM, root_key_pem) - # verify the content with this cert - good_cert = load_certificate(FILETYPE_PEM, root_cert_pem) - # certificate unrelated to priv_key, used to trigger an error - bad_cert = load_certificate(FILETYPE_PEM, server_cert_pem) - - for digest in ['md5', 'sha1']: - sig = sign(priv_key, content, digest) - - # Verify the signature of content, will throw an exception if error. - verify(good_cert, sig, content, digest) - - # This should fail because the certificate doesn't match the - # private key that was used to sign the content. - self.assertRaises(Error, verify, bad_cert, sig, content, digest) - - # This should fail because we've "tainted" the content after - # signing it. - self.assertRaises( - Error, verify, - good_cert, sig, content + b("tainted"), digest) - - # test that unknown digest types fail - self.assertRaises( - ValueError, sign, priv_key, content, "strange-digest") - self.assertRaises( - ValueError, verify, good_cert, sig, content, "strange-digest") - - - def test_sign_nulls(self): - """ - :py:obj:`sign` produces a signature for a string with embedded nulls. - """ - content = b("Watch out! \0 Did you see it?") - priv_key = load_privatekey(FILETYPE_PEM, root_key_pem) - good_cert = load_certificate(FILETYPE_PEM, root_cert_pem) - sig = sign(priv_key, content, "sha1") - verify(good_cert, sig, content, "sha1") - - - -class EllipticCurveTests(TestCase): - """ - Tests for :py:class:`_EllipticCurve`, :py:obj:`get_elliptic_curve`, and - :py:obj:`get_elliptic_curves`. - """ - def test_set(self): - """ - :py:obj:`get_elliptic_curves` returns a :py:obj:`set`. - """ - self.assertIsInstance(get_elliptic_curves(), set) - - - def test_some_curves(self): - """ - If :py:mod:`cryptography` has elliptic curve support then the set - returned by :py:obj:`get_elliptic_curves` has some elliptic curves in - it. - - There could be an OpenSSL that violates this assumption. If so, this - test will fail and we'll find out. - """ - curves = get_elliptic_curves() - if lib.Cryptography_HAS_EC: - self.assertTrue(curves) - else: - self.assertFalse(curves) - - - def test_a_curve(self): - """ - :py:obj:`get_elliptic_curve` can be used to retrieve a particular - supported curve. - """ - curves = get_elliptic_curves() - if curves: - curve = next(iter(curves)) - self.assertEqual(curve.name, get_elliptic_curve(curve.name).name) - else: - self.assertRaises(ValueError, get_elliptic_curve, u("prime256v1")) - - - def test_not_a_curve(self): - """ - :py:obj:`get_elliptic_curve` raises :py:class:`ValueError` if called - with a name which does not identify a supported curve. - """ - self.assertRaises( - ValueError, get_elliptic_curve, u("this curve was just invented")) - - - def test_repr(self): - """ - The string representation of a curve object includes simply states the - object is a curve and what its name is. - """ - curves = get_elliptic_curves() - if curves: - curve = next(iter(curves)) - self.assertEqual("" % (curve.name,), repr(curve)) - - - def test_to_EC_KEY(self): - """ - The curve object can export a version of itself as an EC_KEY* via the - private :py:meth:`_EllipticCurve._to_EC_KEY`. - """ - curves = get_elliptic_curves() - if curves: - curve = next(iter(curves)) - # It's not easy to assert anything about this object. However, see - # leakcheck/crypto.py for a test that demonstrates it at least does - # not leak memory. - curve._to_EC_KEY() - - - -class EllipticCurveFactory(object): - """ - A helper to get the names of two curves. - """ - def __init__(self): - curves = iter(get_elliptic_curves()) - try: - self.curve_name = next(curves).name - self.another_curve_name = next(curves).name - except StopIteration: - self.curve_name = self.another_curve_name = None - - - -class EllipticCurveEqualityTests(TestCase, EqualityTestsMixin): - """ - Tests :py:type:`_EllipticCurve`\ 's implementation of ``==`` and ``!=``. - """ - curve_factory = EllipticCurveFactory() - - if curve_factory.curve_name is None: - skip = "There are no curves available there can be no curve objects." - - - def anInstance(self): - """ - Get the curve object for an arbitrary curve supported by the system. - """ - return get_elliptic_curve(self.curve_factory.curve_name) - - - def anotherInstance(self): - """ - Get the curve object for an arbitrary curve supported by the system - - but not the one returned by C{anInstance}. - """ - return get_elliptic_curve(self.curve_factory.another_curve_name) - - - -class EllipticCurveHashTests(TestCase): - """ - Tests for :py:type:`_EllipticCurve`\ 's implementation of hashing (thus use - as an item in a :py:type:`dict` or :py:type:`set`). - """ - curve_factory = EllipticCurveFactory() - - if curve_factory.curve_name is None: - skip = "There are no curves available there can be no curve objects." - - - def test_contains(self): - """ - The ``in`` operator reports that a :py:type:`set` containing a curve - does contain that curve. - """ - curve = get_elliptic_curve(self.curve_factory.curve_name) - curves = set([curve]) - self.assertIn(curve, curves) - - - def test_does_not_contain(self): - """ - The ``in`` operator reports that a :py:type:`set` not containing a - curve does not contain that curve. - """ - curve = get_elliptic_curve(self.curve_factory.curve_name) - curves = set([get_elliptic_curve(self.curve_factory.another_curve_name)]) - self.assertNotIn(curve, curves) - - - -if __name__ == '__main__': - main() diff --git a/OpenSSL/test/test_rand.py b/OpenSSL/test/test_rand.py deleted file mode 100644 index c52cb6b90..000000000 --- a/OpenSSL/test/test_rand.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright (c) Frederick Dean -# See LICENSE for details. - -""" -Unit tests for :py:obj:`OpenSSL.rand`. -""" - -from unittest import main -import os -import stat -import sys - -from OpenSSL.test.util import TestCase, b -from OpenSSL import rand - - -class RandTests(TestCase): - def test_bytes_wrong_args(self): - """ - :py:obj:`OpenSSL.rand.bytes` raises :py:obj:`TypeError` if called with the wrong - number of arguments or with a non-:py:obj:`int` argument. - """ - self.assertRaises(TypeError, rand.bytes) - self.assertRaises(TypeError, rand.bytes, None) - self.assertRaises(TypeError, rand.bytes, 3, None) - - - def test_insufficientMemory(self): - """ - :py:obj:`OpenSSL.rand.bytes` raises :py:obj:`MemoryError` if more bytes - are requested than will fit in memory. - """ - self.assertRaises(MemoryError, rand.bytes, sys.maxsize) - - - def test_bytes(self): - """ - Verify that we can obtain bytes from rand_bytes() and - that they are different each time. Test the parameter - of rand_bytes() for bad values. - """ - b1 = rand.bytes(50) - self.assertEqual(len(b1), 50) - b2 = rand.bytes(num_bytes=50) # parameter by name - self.assertNotEqual(b1, b2) # Hip, Hip, Horay! FIPS complaince - b3 = rand.bytes(num_bytes=0) - self.assertEqual(len(b3), 0) - exc = self.assertRaises(ValueError, rand.bytes, -1) - self.assertEqual(str(exc), "num_bytes must not be negative") - - - def test_add_wrong_args(self): - """ - When called with the wrong number of arguments, or with arguments not of - type :py:obj:`str` and :py:obj:`int`, :py:obj:`OpenSSL.rand.add` raises :py:obj:`TypeError`. - """ - self.assertRaises(TypeError, rand.add) - self.assertRaises(TypeError, rand.add, b("foo"), None) - self.assertRaises(TypeError, rand.add, None, 3) - self.assertRaises(TypeError, rand.add, b("foo"), 3, None) - - - def test_add(self): - """ - :py:obj:`OpenSSL.rand.add` adds entropy to the PRNG. - """ - rand.add(b('hamburger'), 3) - - - def test_seed_wrong_args(self): - """ - When called with the wrong number of arguments, or with a non-:py:obj:`str` - argument, :py:obj:`OpenSSL.rand.seed` raises :py:obj:`TypeError`. - """ - self.assertRaises(TypeError, rand.seed) - self.assertRaises(TypeError, rand.seed, None) - self.assertRaises(TypeError, rand.seed, b("foo"), None) - - - def test_seed(self): - """ - :py:obj:`OpenSSL.rand.seed` adds entropy to the PRNG. - """ - rand.seed(b('milk shake')) - - - def test_status_wrong_args(self): - """ - :py:obj:`OpenSSL.rand.status` raises :py:obj:`TypeError` when called with any - arguments. - """ - self.assertRaises(TypeError, rand.status, None) - - - def test_status(self): - """ - :py:obj:`OpenSSL.rand.status` returns :py:obj:`True` if the PRNG has sufficient - entropy, :py:obj:`False` otherwise. - """ - # It's hard to know what it is actually going to return. Different - # OpenSSL random engines decide differently whether they have enough - # entropy or not. - self.assertTrue(rand.status() in (1, 2)) - - - def test_egd_wrong_args(self): - """ - :py:obj:`OpenSSL.rand.egd` raises :py:obj:`TypeError` when called with the wrong - number of arguments or with arguments not of type :py:obj:`str` and :py:obj:`int`. - """ - self.assertRaises(TypeError, rand.egd) - self.assertRaises(TypeError, rand.egd, None) - self.assertRaises(TypeError, rand.egd, "foo", None) - self.assertRaises(TypeError, rand.egd, None, 3) - self.assertRaises(TypeError, rand.egd, "foo", 3, None) - - - def test_egd_missing(self): - """ - :py:obj:`OpenSSL.rand.egd` returns :py:obj:`0` or :py:obj:`-1` if the - EGD socket passed to it does not exist. - """ - result = rand.egd(self.mktemp()) - expected = (-1, 0) - self.assertTrue( - result in expected, - "%r not in %r" % (result, expected)) - - - def test_egd_missing_and_bytes(self): - """ - :py:obj:`OpenSSL.rand.egd` returns :py:obj:`0` or :py:obj:`-1` if the - EGD socket passed to it does not exist even if a size argument is - explicitly passed. - """ - result = rand.egd(self.mktemp(), 1024) - expected = (-1, 0) - self.assertTrue( - result in expected, - "%r not in %r" % (result, expected)) - - - def test_cleanup_wrong_args(self): - """ - :py:obj:`OpenSSL.rand.cleanup` raises :py:obj:`TypeError` when called with any - arguments. - """ - self.assertRaises(TypeError, rand.cleanup, None) - - - def test_cleanup(self): - """ - :py:obj:`OpenSSL.rand.cleanup` releases the memory used by the PRNG and returns - :py:obj:`None`. - """ - self.assertIdentical(rand.cleanup(), None) - - - def test_load_file_wrong_args(self): - """ - :py:obj:`OpenSSL.rand.load_file` raises :py:obj:`TypeError` when called the wrong - number of arguments or arguments not of type :py:obj:`str` and :py:obj:`int`. - """ - self.assertRaises(TypeError, rand.load_file) - self.assertRaises(TypeError, rand.load_file, "foo", None) - self.assertRaises(TypeError, rand.load_file, None, 1) - self.assertRaises(TypeError, rand.load_file, "foo", 1, None) - - - def test_write_file_wrong_args(self): - """ - :py:obj:`OpenSSL.rand.write_file` raises :py:obj:`TypeError` when called with the - wrong number of arguments or a non-:py:obj:`str` argument. - """ - self.assertRaises(TypeError, rand.write_file) - self.assertRaises(TypeError, rand.write_file, None) - self.assertRaises(TypeError, rand.write_file, "foo", None) - - - def test_files(self): - """ - Test reading and writing of files via rand functions. - """ - # Write random bytes to a file - tmpfile = self.mktemp() - # Make sure it exists (so cleanup definitely succeeds) - fObj = open(tmpfile, 'w') - fObj.close() - try: - rand.write_file(tmpfile) - # Verify length of written file - size = os.stat(tmpfile)[stat.ST_SIZE] - self.assertEqual(1024, size) - # Read random bytes from file - rand.load_file(tmpfile) - rand.load_file(tmpfile, 4) # specify a length - finally: - # Cleanup - os.unlink(tmpfile) - - -if __name__ == '__main__': - main() diff --git a/OpenSSL/test/test_ssl.py b/OpenSSL/test/test_ssl.py deleted file mode 100644 index 6409b8ee1..000000000 --- a/OpenSSL/test/test_ssl.py +++ /dev/null @@ -1,2964 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. - -""" -Unit tests for :py:obj:`OpenSSL.SSL`. -""" - -from gc import collect, get_referrers -from errno import ECONNREFUSED, EINPROGRESS, EWOULDBLOCK, EPIPE, ESHUTDOWN -from sys import platform, version_info -from socket import SHUT_RDWR, error, socket -from os import makedirs -from os.path import join -from unittest import main -from weakref import ref - -from six import PY3, text_type, u - -from OpenSSL.crypto import TYPE_RSA, FILETYPE_PEM -from OpenSSL.crypto import PKey, X509, X509Extension, X509Store -from OpenSSL.crypto import dump_privatekey, load_privatekey -from OpenSSL.crypto import dump_certificate, load_certificate -from OpenSSL.crypto import get_elliptic_curves - -from OpenSSL.SSL import _lib -from OpenSSL.SSL import OPENSSL_VERSION_NUMBER, SSLEAY_VERSION, SSLEAY_CFLAGS -from OpenSSL.SSL import SSLEAY_PLATFORM, SSLEAY_DIR, SSLEAY_BUILT_ON -from OpenSSL.SSL import SENT_SHUTDOWN, RECEIVED_SHUTDOWN -from OpenSSL.SSL import ( - SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD, - TLSv1_1_METHOD, TLSv1_2_METHOD) -from OpenSSL.SSL import OP_SINGLE_DH_USE, OP_NO_SSLv2, OP_NO_SSLv3 -from OpenSSL.SSL import ( - VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT, VERIFY_CLIENT_ONCE, VERIFY_NONE) - -from OpenSSL.SSL import ( - SESS_CACHE_OFF, SESS_CACHE_CLIENT, SESS_CACHE_SERVER, SESS_CACHE_BOTH, - SESS_CACHE_NO_AUTO_CLEAR, SESS_CACHE_NO_INTERNAL_LOOKUP, - SESS_CACHE_NO_INTERNAL_STORE, SESS_CACHE_NO_INTERNAL) - -from OpenSSL.SSL import ( - Error, SysCallError, WantReadError, WantWriteError, ZeroReturnError) -from OpenSSL.SSL import ( - Context, ContextType, Session, Connection, ConnectionType, SSLeay_version) - -from OpenSSL.test.util import TestCase, b -from OpenSSL.test.test_crypto import ( - cleartextCertificatePEM, cleartextPrivateKeyPEM) -from OpenSSL.test.test_crypto import ( - client_cert_pem, client_key_pem, server_cert_pem, server_key_pem, - root_cert_pem) - -try: - from OpenSSL.SSL import OP_NO_QUERY_MTU -except ImportError: - OP_NO_QUERY_MTU = None -try: - from OpenSSL.SSL import OP_COOKIE_EXCHANGE -except ImportError: - OP_COOKIE_EXCHANGE = None -try: - from OpenSSL.SSL import OP_NO_TICKET -except ImportError: - OP_NO_TICKET = None - -try: - from OpenSSL.SSL import OP_NO_COMPRESSION -except ImportError: - OP_NO_COMPRESSION = None - -try: - from OpenSSL.SSL import MODE_RELEASE_BUFFERS -except ImportError: - MODE_RELEASE_BUFFERS = None - -try: - from OpenSSL.SSL import OP_NO_TLSv1, OP_NO_TLSv1_1, OP_NO_TLSv1_2 -except ImportError: - OP_NO_TLSv1 = OP_NO_TLSv1_1 = OP_NO_TLSv1_2 = None - -from OpenSSL.SSL import ( - SSL_ST_CONNECT, SSL_ST_ACCEPT, SSL_ST_MASK, SSL_ST_INIT, SSL_ST_BEFORE, - SSL_ST_OK, SSL_ST_RENEGOTIATE, - SSL_CB_LOOP, SSL_CB_EXIT, SSL_CB_READ, SSL_CB_WRITE, SSL_CB_ALERT, - SSL_CB_READ_ALERT, SSL_CB_WRITE_ALERT, SSL_CB_ACCEPT_LOOP, - SSL_CB_ACCEPT_EXIT, SSL_CB_CONNECT_LOOP, SSL_CB_CONNECT_EXIT, - SSL_CB_HANDSHAKE_START, SSL_CB_HANDSHAKE_DONE) - -# openssl dhparam 128 -out dh-128.pem (note that 128 is a small number of bits -# to use) -dhparam = """\ ------BEGIN DH PARAMETERS----- -MBYCEQCobsg29c9WZP/54oAPcwiDAgEC ------END DH PARAMETERS----- -""" - - -def verify_cb(conn, cert, errnum, depth, ok): - return ok - - -def socket_pair(): - """ - Establish and return a pair of network sockets connected to each other. - """ - # Connect a pair of sockets - port = socket() - port.bind(('', 0)) - port.listen(1) - client = socket() - client.setblocking(False) - client.connect_ex(("127.0.0.1", port.getsockname()[1])) - client.setblocking(True) - server = port.accept()[0] - - # Let's pass some unencrypted data to make sure our socket connection is - # fine. Just one byte, so we don't have to worry about buffers getting - # filled up or fragmentation. - server.send(b("x")) - assert client.recv(1024) == b("x") - client.send(b("y")) - assert server.recv(1024) == b("y") - - # Most of our callers want non-blocking sockets, make it easy for them. - server.setblocking(False) - client.setblocking(False) - - return (server, client) - - - -def handshake(client, server): - conns = [client, server] - while conns: - for conn in conns: - try: - conn.do_handshake() - except WantReadError: - pass - else: - conns.remove(conn) - - -def _create_certificate_chain(): - """ - Construct and return a chain of certificates. - - 1. A new self-signed certificate authority certificate (cacert) - 2. A new intermediate certificate signed by cacert (icert) - 3. A new server certificate signed by icert (scert) - """ - caext = X509Extension(b('basicConstraints'), False, b('CA:true')) - - # Step 1 - cakey = PKey() - cakey.generate_key(TYPE_RSA, 512) - cacert = X509() - cacert.get_subject().commonName = "Authority Certificate" - cacert.set_issuer(cacert.get_subject()) - cacert.set_pubkey(cakey) - cacert.set_notBefore(b("20000101000000Z")) - cacert.set_notAfter(b("20200101000000Z")) - cacert.add_extensions([caext]) - cacert.set_serial_number(0) - cacert.sign(cakey, "sha1") - - # Step 2 - ikey = PKey() - ikey.generate_key(TYPE_RSA, 512) - icert = X509() - icert.get_subject().commonName = "Intermediate Certificate" - icert.set_issuer(cacert.get_subject()) - icert.set_pubkey(ikey) - icert.set_notBefore(b("20000101000000Z")) - icert.set_notAfter(b("20200101000000Z")) - icert.add_extensions([caext]) - icert.set_serial_number(0) - icert.sign(cakey, "sha1") - - # Step 3 - skey = PKey() - skey.generate_key(TYPE_RSA, 512) - scert = X509() - scert.get_subject().commonName = "Server Certificate" - scert.set_issuer(icert.get_subject()) - scert.set_pubkey(skey) - scert.set_notBefore(b("20000101000000Z")) - scert.set_notAfter(b("20200101000000Z")) - scert.add_extensions([ - X509Extension(b('basicConstraints'), True, b('CA:false'))]) - scert.set_serial_number(0) - scert.sign(ikey, "sha1") - - return [(cakey, cacert), (ikey, icert), (skey, scert)] - - - -class _LoopbackMixin: - """ - Helper mixin which defines methods for creating a connected socket pair and - for forcing two connected SSL sockets to talk to each other via memory BIOs. - """ - def _loopbackClientFactory(self, socket): - client = Connection(Context(TLSv1_METHOD), socket) - client.set_connect_state() - return client - - - def _loopbackServerFactory(self, socket): - ctx = Context(TLSv1_METHOD) - ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) - ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) - server = Connection(ctx, socket) - server.set_accept_state() - return server - - - def _loopback(self, serverFactory=None, clientFactory=None): - if serverFactory is None: - serverFactory = self._loopbackServerFactory - if clientFactory is None: - clientFactory = self._loopbackClientFactory - - (server, client) = socket_pair() - server = serverFactory(server) - client = clientFactory(client) - - handshake(client, server) - - server.setblocking(True) - client.setblocking(True) - return server, client - - - def _interactInMemory(self, client_conn, server_conn): - """ - Try to read application bytes from each of the two :py:obj:`Connection` - objects. Copy bytes back and forth between their send/receive buffers - for as long as there is anything to copy. When there is nothing more - to copy, return :py:obj:`None`. If one of them actually manages to deliver - some application bytes, return a two-tuple of the connection from which - the bytes were read and the bytes themselves. - """ - wrote = True - while wrote: - # Loop until neither side has anything to say - wrote = False - - # Copy stuff from each side's send buffer to the other side's - # receive buffer. - for (read, write) in [(client_conn, server_conn), - (server_conn, client_conn)]: - - # Give the side a chance to generate some more bytes, or - # succeed. - try: - data = read.recv(2 ** 16) - except WantReadError: - # It didn't succeed, so we'll hope it generated some - # output. - pass - else: - # It did succeed, so we'll stop now and let the caller deal - # with it. - return (read, data) - - while True: - # Keep copying as long as there's more stuff there. - try: - dirty = read.bio_read(4096) - except WantReadError: - # Okay, nothing more waiting to be sent. Stop - # processing this send buffer. - break - else: - # Keep track of the fact that someone generated some - # output. - wrote = True - write.bio_write(dirty) - - - def _handshakeInMemory(self, client_conn, server_conn): - """ - Perform the TLS handshake between two :py:class:`Connection` instances - connected to each other via memory BIOs. - """ - client_conn.set_connect_state() - server_conn.set_accept_state() - - for conn in [client_conn, server_conn]: - try: - conn.do_handshake() - except WantReadError: - pass - - self._interactInMemory(client_conn, server_conn) - - - -class VersionTests(TestCase): - """ - Tests for version information exposed by - :py:obj:`OpenSSL.SSL.SSLeay_version` and - :py:obj:`OpenSSL.SSL.OPENSSL_VERSION_NUMBER`. - """ - def test_OPENSSL_VERSION_NUMBER(self): - """ - :py:obj:`OPENSSL_VERSION_NUMBER` is an integer with status in the low - byte and the patch, fix, minor, and major versions in the - nibbles above that. - """ - self.assertTrue(isinstance(OPENSSL_VERSION_NUMBER, int)) - - - def test_SSLeay_version(self): - """ - :py:obj:`SSLeay_version` takes a version type indicator and returns - one of a number of version strings based on that indicator. - """ - versions = {} - for t in [SSLEAY_VERSION, SSLEAY_CFLAGS, SSLEAY_BUILT_ON, - SSLEAY_PLATFORM, SSLEAY_DIR]: - version = SSLeay_version(t) - versions[version] = t - self.assertTrue(isinstance(version, bytes)) - self.assertEqual(len(versions), 5) - - - -class ContextTests(TestCase, _LoopbackMixin): - """ - Unit tests for :py:obj:`OpenSSL.SSL.Context`. - """ - def test_method(self): - """ - :py:obj:`Context` can be instantiated with one of :py:obj:`SSLv2_METHOD`, - :py:obj:`SSLv3_METHOD`, :py:obj:`SSLv23_METHOD`, :py:obj:`TLSv1_METHOD`, - :py:obj:`TLSv1_1_METHOD`, or :py:obj:`TLSv1_2_METHOD`. - """ - methods = [ - SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD] - for meth in methods: - Context(meth) - - - maybe = [SSLv2_METHOD, TLSv1_1_METHOD, TLSv1_2_METHOD] - for meth in maybe: - try: - Context(meth) - except (Error, ValueError): - # Some versions of OpenSSL have SSLv2 / TLSv1.1 / TLSv1.2, some - # don't. Difficult to say in advance. - pass - - self.assertRaises(TypeError, Context, "") - self.assertRaises(ValueError, Context, 10) - - - if not PY3: - def test_method_long(self): - """ - On Python 2 :py:class:`Context` accepts values of type - :py:obj:`long` as well as :py:obj:`int`. - """ - Context(long(TLSv1_METHOD)) - - - - def test_type(self): - """ - :py:obj:`Context` and :py:obj:`ContextType` refer to the same type object and can be - used to create instances of that type. - """ - self.assertIdentical(Context, ContextType) - self.assertConsistentType(Context, 'Context', TLSv1_METHOD) - - - def test_use_privatekey(self): - """ - :py:obj:`Context.use_privatekey` takes an :py:obj:`OpenSSL.crypto.PKey` instance. - """ - key = PKey() - key.generate_key(TYPE_RSA, 128) - ctx = Context(TLSv1_METHOD) - ctx.use_privatekey(key) - self.assertRaises(TypeError, ctx.use_privatekey, "") - - - def test_use_privatekey_file_missing(self): - """ - :py:obj:`Context.use_privatekey_file` raises :py:obj:`OpenSSL.SSL.Error` - when passed the name of a file which does not exist. - """ - ctx = Context(TLSv1_METHOD) - self.assertRaises(Error, ctx.use_privatekey_file, self.mktemp()) - - - if not PY3: - def test_use_privatekey_file_long(self): - """ - On Python 2 :py:obj:`Context.use_privatekey_file` accepts a - filetype of type :py:obj:`long` as well as :py:obj:`int`. - """ - pemfile = self.mktemp() - - key = PKey() - key.generate_key(TYPE_RSA, 128) - - with open(pemfile, "wt") as pem: - pem.write( - dump_privatekey(FILETYPE_PEM, key).decode("ascii")) - - ctx = Context(TLSv1_METHOD) - ctx.use_privatekey_file(pemfile, long(FILETYPE_PEM)) - - - def test_use_certificate_wrong_args(self): - """ - :py:obj:`Context.use_certificate_wrong_args` raises :py:obj:`TypeError` - when not passed exactly one :py:obj:`OpenSSL.crypto.X509` instance as an - argument. - """ - ctx = Context(TLSv1_METHOD) - self.assertRaises(TypeError, ctx.use_certificate) - self.assertRaises(TypeError, ctx.use_certificate, "hello, world") - self.assertRaises(TypeError, ctx.use_certificate, X509(), "hello, world") - - - def test_use_certificate_uninitialized(self): - """ - :py:obj:`Context.use_certificate` raises :py:obj:`OpenSSL.SSL.Error` - when passed a :py:obj:`OpenSSL.crypto.X509` instance which has not been - initialized (ie, which does not actually have any certificate data). - """ - ctx = Context(TLSv1_METHOD) - self.assertRaises(Error, ctx.use_certificate, X509()) - - - def test_use_certificate(self): - """ - :py:obj:`Context.use_certificate` sets the certificate which will be - used to identify connections created using the context. - """ - # TODO - # Hard to assert anything. But we could set a privatekey then ask - # OpenSSL if the cert and key agree using check_privatekey. Then as - # long as check_privatekey works right we're good... - ctx = Context(TLSv1_METHOD) - ctx.use_certificate(load_certificate(FILETYPE_PEM, cleartextCertificatePEM)) - - - def test_use_certificate_file_wrong_args(self): - """ - :py:obj:`Context.use_certificate_file` raises :py:obj:`TypeError` if - called with zero arguments or more than two arguments, or if the first - argument is not a byte string or the second argumnent is not an integer. - """ - ctx = Context(TLSv1_METHOD) - self.assertRaises(TypeError, ctx.use_certificate_file) - self.assertRaises(TypeError, ctx.use_certificate_file, b"somefile", object()) - self.assertRaises( - TypeError, ctx.use_certificate_file, b"somefile", FILETYPE_PEM, object()) - self.assertRaises( - TypeError, ctx.use_certificate_file, object(), FILETYPE_PEM) - self.assertRaises( - TypeError, ctx.use_certificate_file, b"somefile", object()) - - - def test_use_certificate_file_missing(self): - """ - :py:obj:`Context.use_certificate_file` raises - `:py:obj:`OpenSSL.SSL.Error` if passed the name of a file which does not - exist. - """ - ctx = Context(TLSv1_METHOD) - self.assertRaises(Error, ctx.use_certificate_file, self.mktemp()) - - - def test_use_certificate_file(self): - """ - :py:obj:`Context.use_certificate` sets the certificate which will be - used to identify connections created using the context. - """ - # TODO - # Hard to assert anything. But we could set a privatekey then ask - # OpenSSL if the cert and key agree using check_privatekey. Then as - # long as check_privatekey works right we're good... - pem_filename = self.mktemp() - with open(pem_filename, "wb") as pem_file: - pem_file.write(cleartextCertificatePEM) - - ctx = Context(TLSv1_METHOD) - ctx.use_certificate_file(pem_filename) - - - if not PY3: - def test_use_certificate_file_long(self): - """ - On Python 2 :py:obj:`Context.use_certificate_file` accepts a - filetype of type :py:obj:`long` as well as :py:obj:`int`. - """ - pem_filename = self.mktemp() - with open(pem_filename, "wb") as pem_file: - pem_file.write(cleartextCertificatePEM) - - ctx = Context(TLSv1_METHOD) - ctx.use_certificate_file(pem_filename, long(FILETYPE_PEM)) - - - def test_set_app_data_wrong_args(self): - """ - :py:obj:`Context.set_app_data` raises :py:obj:`TypeError` if called with other than - one argument. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.set_app_data) - self.assertRaises(TypeError, context.set_app_data, None, None) - - - def test_get_app_data_wrong_args(self): - """ - :py:obj:`Context.get_app_data` raises :py:obj:`TypeError` if called with any - arguments. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.get_app_data, None) - - - def test_app_data(self): - """ - :py:obj:`Context.set_app_data` stores an object for later retrieval using - :py:obj:`Context.get_app_data`. - """ - app_data = object() - context = Context(TLSv1_METHOD) - context.set_app_data(app_data) - self.assertIdentical(context.get_app_data(), app_data) - - - def test_set_options_wrong_args(self): - """ - :py:obj:`Context.set_options` raises :py:obj:`TypeError` if called with the wrong - number of arguments or a non-:py:obj:`int` argument. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.set_options) - self.assertRaises(TypeError, context.set_options, None) - self.assertRaises(TypeError, context.set_options, 1, None) - - - def test_set_options(self): - """ - :py:obj:`Context.set_options` returns the new options value. - """ - context = Context(TLSv1_METHOD) - options = context.set_options(OP_NO_SSLv2) - self.assertTrue(OP_NO_SSLv2 & options) - - - if not PY3: - def test_set_options_long(self): - """ - On Python 2 :py:obj:`Context.set_options` accepts values of type - :py:obj:`long` as well as :py:obj:`int`. - """ - context = Context(TLSv1_METHOD) - options = context.set_options(long(OP_NO_SSLv2)) - self.assertTrue(OP_NO_SSLv2 & options) - - - def test_set_mode_wrong_args(self): - """ - :py:obj:`Context.set`mode} raises :py:obj:`TypeError` if called with the wrong - number of arguments or a non-:py:obj:`int` argument. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.set_mode) - self.assertRaises(TypeError, context.set_mode, None) - self.assertRaises(TypeError, context.set_mode, 1, None) - - - if MODE_RELEASE_BUFFERS is not None: - def test_set_mode(self): - """ - :py:obj:`Context.set_mode` accepts a mode bitvector and returns the newly - set mode. - """ - context = Context(TLSv1_METHOD) - self.assertTrue( - MODE_RELEASE_BUFFERS & context.set_mode(MODE_RELEASE_BUFFERS)) - - if not PY3: - def test_set_mode_long(self): - """ - On Python 2 :py:obj:`Context.set_mode` accepts values of type - :py:obj:`long` as well as :py:obj:`int`. - """ - context = Context(TLSv1_METHOD) - mode = context.set_mode(long(MODE_RELEASE_BUFFERS)) - self.assertTrue(MODE_RELEASE_BUFFERS & mode) - else: - "MODE_RELEASE_BUFFERS unavailable - OpenSSL version may be too old" - - - def test_set_timeout_wrong_args(self): - """ - :py:obj:`Context.set_timeout` raises :py:obj:`TypeError` if called with the wrong - number of arguments or a non-:py:obj:`int` argument. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.set_timeout) - self.assertRaises(TypeError, context.set_timeout, None) - self.assertRaises(TypeError, context.set_timeout, 1, None) - - - def test_get_timeout_wrong_args(self): - """ - :py:obj:`Context.get_timeout` raises :py:obj:`TypeError` if called with any arguments. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.get_timeout, None) - - - def test_timeout(self): - """ - :py:obj:`Context.set_timeout` sets the session timeout for all connections - created using the context object. :py:obj:`Context.get_timeout` retrieves this - value. - """ - context = Context(TLSv1_METHOD) - context.set_timeout(1234) - self.assertEquals(context.get_timeout(), 1234) - - - if not PY3: - def test_timeout_long(self): - """ - On Python 2 :py:obj:`Context.set_timeout` accepts values of type - `long` as well as int. - """ - context = Context(TLSv1_METHOD) - context.set_timeout(long(1234)) - self.assertEquals(context.get_timeout(), 1234) - - - def test_set_verify_depth_wrong_args(self): - """ - :py:obj:`Context.set_verify_depth` raises :py:obj:`TypeError` if called with the wrong - number of arguments or a non-:py:obj:`int` argument. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.set_verify_depth) - self.assertRaises(TypeError, context.set_verify_depth, None) - self.assertRaises(TypeError, context.set_verify_depth, 1, None) - - - def test_get_verify_depth_wrong_args(self): - """ - :py:obj:`Context.get_verify_depth` raises :py:obj:`TypeError` if called with any arguments. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.get_verify_depth, None) - - - def test_verify_depth(self): - """ - :py:obj:`Context.set_verify_depth` sets the number of certificates in a chain - to follow before giving up. The value can be retrieved with - :py:obj:`Context.get_verify_depth`. - """ - context = Context(TLSv1_METHOD) - context.set_verify_depth(11) - self.assertEquals(context.get_verify_depth(), 11) - - - if not PY3: - def test_verify_depth_long(self): - """ - On Python 2 :py:obj:`Context.set_verify_depth` accepts values of - type `long` as well as int. - """ - context = Context(TLSv1_METHOD) - context.set_verify_depth(long(11)) - self.assertEquals(context.get_verify_depth(), 11) - - - def _write_encrypted_pem(self, passphrase): - """ - Write a new private key out to a new file, encrypted using the given - passphrase. Return the path to the new file. - """ - key = PKey() - key.generate_key(TYPE_RSA, 128) - pemFile = self.mktemp() - fObj = open(pemFile, 'w') - pem = dump_privatekey(FILETYPE_PEM, key, "blowfish", passphrase) - fObj.write(pem.decode('ascii')) - fObj.close() - return pemFile - - - def test_set_passwd_cb_wrong_args(self): - """ - :py:obj:`Context.set_passwd_cb` raises :py:obj:`TypeError` if called with the - wrong arguments or with a non-callable first argument. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.set_passwd_cb) - self.assertRaises(TypeError, context.set_passwd_cb, None) - self.assertRaises(TypeError, context.set_passwd_cb, lambda: None, None, None) - - - def test_set_passwd_cb(self): - """ - :py:obj:`Context.set_passwd_cb` accepts a callable which will be invoked when - a private key is loaded from an encrypted PEM. - """ - passphrase = b("foobar") - pemFile = self._write_encrypted_pem(passphrase) - calledWith = [] - def passphraseCallback(maxlen, verify, extra): - calledWith.append((maxlen, verify, extra)) - return passphrase - context = Context(TLSv1_METHOD) - context.set_passwd_cb(passphraseCallback) - context.use_privatekey_file(pemFile) - self.assertTrue(len(calledWith), 1) - self.assertTrue(isinstance(calledWith[0][0], int)) - self.assertTrue(isinstance(calledWith[0][1], int)) - self.assertEqual(calledWith[0][2], None) - - - def test_passwd_callback_exception(self): - """ - :py:obj:`Context.use_privatekey_file` propagates any exception raised by the - passphrase callback. - """ - pemFile = self._write_encrypted_pem(b("monkeys are nice")) - def passphraseCallback(maxlen, verify, extra): - raise RuntimeError("Sorry, I am a fail.") - - context = Context(TLSv1_METHOD) - context.set_passwd_cb(passphraseCallback) - self.assertRaises(RuntimeError, context.use_privatekey_file, pemFile) - - - def test_passwd_callback_false(self): - """ - :py:obj:`Context.use_privatekey_file` raises :py:obj:`OpenSSL.SSL.Error` if the - passphrase callback returns a false value. - """ - pemFile = self._write_encrypted_pem(b("monkeys are nice")) - def passphraseCallback(maxlen, verify, extra): - return b"" - - context = Context(TLSv1_METHOD) - context.set_passwd_cb(passphraseCallback) - self.assertRaises(Error, context.use_privatekey_file, pemFile) - - - def test_passwd_callback_non_string(self): - """ - :py:obj:`Context.use_privatekey_file` raises :py:obj:`OpenSSL.SSL.Error` if the - passphrase callback returns a true non-string value. - """ - pemFile = self._write_encrypted_pem(b("monkeys are nice")) - def passphraseCallback(maxlen, verify, extra): - return 10 - - context = Context(TLSv1_METHOD) - context.set_passwd_cb(passphraseCallback) - self.assertRaises(ValueError, context.use_privatekey_file, pemFile) - - - def test_passwd_callback_too_long(self): - """ - If the passphrase returned by the passphrase callback returns a string - longer than the indicated maximum length, it is truncated. - """ - # A priori knowledge! - passphrase = b("x") * 1024 - pemFile = self._write_encrypted_pem(passphrase) - def passphraseCallback(maxlen, verify, extra): - assert maxlen == 1024 - return passphrase + b("y") - - context = Context(TLSv1_METHOD) - context.set_passwd_cb(passphraseCallback) - # This shall succeed because the truncated result is the correct - # passphrase. - context.use_privatekey_file(pemFile) - - - def test_set_info_callback(self): - """ - :py:obj:`Context.set_info_callback` accepts a callable which will be invoked - when certain information about an SSL connection is available. - """ - (server, client) = socket_pair() - - clientSSL = Connection(Context(TLSv1_METHOD), client) - clientSSL.set_connect_state() - - called = [] - def info(conn, where, ret): - called.append((conn, where, ret)) - context = Context(TLSv1_METHOD) - context.set_info_callback(info) - context.use_certificate( - load_certificate(FILETYPE_PEM, cleartextCertificatePEM)) - context.use_privatekey( - load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)) - - serverSSL = Connection(context, server) - serverSSL.set_accept_state() - - handshake(clientSSL, serverSSL) - - # The callback must always be called with a Connection instance as the - # first argument. It would probably be better to split this into - # separate tests for client and server side info callbacks so we could - # assert it is called with the right Connection instance. It would - # also be good to assert *something* about `where` and `ret`. - notConnections = [ - conn for (conn, where, ret) in called - if not isinstance(conn, Connection)] - self.assertEqual( - [], notConnections, - "Some info callback arguments were not Connection instaces.") - - - def _load_verify_locations_test(self, *args): - """ - Create a client context which will verify the peer certificate and call - its :py:obj:`load_verify_locations` method with the given arguments. - Then connect it to a server and ensure that the handshake succeeds. - """ - (server, client) = socket_pair() - - clientContext = Context(TLSv1_METHOD) - clientContext.load_verify_locations(*args) - # Require that the server certificate verify properly or the - # connection will fail. - clientContext.set_verify( - VERIFY_PEER, - lambda conn, cert, errno, depth, preverify_ok: preverify_ok) - - clientSSL = Connection(clientContext, client) - clientSSL.set_connect_state() - - serverContext = Context(TLSv1_METHOD) - serverContext.use_certificate( - load_certificate(FILETYPE_PEM, cleartextCertificatePEM)) - serverContext.use_privatekey( - load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)) - - serverSSL = Connection(serverContext, server) - serverSSL.set_accept_state() - - # Without load_verify_locations above, the handshake - # will fail: - # Error: [('SSL routines', 'SSL3_GET_SERVER_CERTIFICATE', - # 'certificate verify failed')] - handshake(clientSSL, serverSSL) - - cert = clientSSL.get_peer_certificate() - self.assertEqual(cert.get_subject().CN, 'Testing Root CA') - - - def test_load_verify_file(self): - """ - :py:obj:`Context.load_verify_locations` accepts a file name and uses the - certificates within for verification purposes. - """ - cafile = self.mktemp() - fObj = open(cafile, 'w') - fObj.write(cleartextCertificatePEM.decode('ascii')) - fObj.close() - - self._load_verify_locations_test(cafile) - - - def test_load_verify_invalid_file(self): - """ - :py:obj:`Context.load_verify_locations` raises :py:obj:`Error` when passed a - non-existent cafile. - """ - clientContext = Context(TLSv1_METHOD) - self.assertRaises( - Error, clientContext.load_verify_locations, self.mktemp()) - - - def test_load_verify_directory(self): - """ - :py:obj:`Context.load_verify_locations` accepts a directory name and uses - the certificates within for verification purposes. - """ - capath = self.mktemp() - makedirs(capath) - # Hash values computed manually with c_rehash to avoid depending on - # c_rehash in the test suite. One is from OpenSSL 0.9.8, the other - # from OpenSSL 1.0.0. - for name in [b'c7adac82.0', b'c3705638.0']: - cafile = join(capath, name) - fObj = open(cafile, 'w') - fObj.write(cleartextCertificatePEM.decode('ascii')) - fObj.close() - - self._load_verify_locations_test(None, capath) - - - def test_load_verify_locations_wrong_args(self): - """ - :py:obj:`Context.load_verify_locations` raises :py:obj:`TypeError` if called with - the wrong number of arguments or with non-:py:obj:`str` arguments. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.load_verify_locations) - self.assertRaises(TypeError, context.load_verify_locations, object()) - self.assertRaises(TypeError, context.load_verify_locations, object(), object()) - self.assertRaises(TypeError, context.load_verify_locations, None, None, None) - - - if platform == "win32": - "set_default_verify_paths appears not to work on Windows. " - "See LP#404343 and LP#404344." - else: - def test_set_default_verify_paths(self): - """ - :py:obj:`Context.set_default_verify_paths` causes the platform-specific CA - certificate locations to be used for verification purposes. - """ - # Testing this requires a server with a certificate signed by one of - # the CAs in the platform CA location. Getting one of those costs - # money. Fortunately (or unfortunately, depending on your - # perspective), it's easy to think of a public server on the - # internet which has such a certificate. Connecting to the network - # in a unit test is bad, but it's the only way I can think of to - # really test this. -exarkun - - # Arg, verisign.com doesn't speak TLSv1 - context = Context(SSLv3_METHOD) - context.set_default_verify_paths() - context.set_verify( - VERIFY_PEER, - lambda conn, cert, errno, depth, preverify_ok: preverify_ok) - - client = socket() - client.connect(('verisign.com', 443)) - clientSSL = Connection(context, client) - clientSSL.set_connect_state() - clientSSL.do_handshake() - clientSSL.send(b"GET / HTTP/1.0\r\n\r\n") - self.assertTrue(clientSSL.recv(1024)) - - - def test_set_default_verify_paths_signature(self): - """ - :py:obj:`Context.set_default_verify_paths` takes no arguments and raises - :py:obj:`TypeError` if given any. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.set_default_verify_paths, None) - self.assertRaises(TypeError, context.set_default_verify_paths, 1) - self.assertRaises(TypeError, context.set_default_verify_paths, "") - - - def test_add_extra_chain_cert_invalid_cert(self): - """ - :py:obj:`Context.add_extra_chain_cert` raises :py:obj:`TypeError` if called with - other than one argument or if called with an object which is not an - instance of :py:obj:`X509`. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.add_extra_chain_cert) - self.assertRaises(TypeError, context.add_extra_chain_cert, object()) - self.assertRaises(TypeError, context.add_extra_chain_cert, object(), object()) - - - def _handshake_test(self, serverContext, clientContext): - """ - Verify that a client and server created with the given contexts can - successfully handshake and communicate. - """ - serverSocket, clientSocket = socket_pair() - - server = Connection(serverContext, serverSocket) - server.set_accept_state() - - client = Connection(clientContext, clientSocket) - client.set_connect_state() - - # Make them talk to each other. - # self._interactInMemory(client, server) - for i in range(3): - for s in [client, server]: - try: - s.do_handshake() - except WantReadError: - pass - - - def test_set_verify_callback_connection_argument(self): - """ - The first argument passed to the verify callback is the - :py:class:`Connection` instance for which verification is taking place. - """ - serverContext = Context(TLSv1_METHOD) - serverContext.use_privatekey( - load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)) - serverContext.use_certificate( - load_certificate(FILETYPE_PEM, cleartextCertificatePEM)) - serverConnection = Connection(serverContext, None) - - class VerifyCallback(object): - def callback(self, connection, *args): - self.connection = connection - return 1 - - verify = VerifyCallback() - clientContext = Context(TLSv1_METHOD) - clientContext.set_verify(VERIFY_PEER, verify.callback) - clientConnection = Connection(clientContext, None) - clientConnection.set_connect_state() - - self._handshakeInMemory(clientConnection, serverConnection) - - self.assertIdentical(verify.connection, clientConnection) - - - def test_set_verify_callback_exception(self): - """ - If the verify callback passed to :py:obj:`Context.set_verify` raises an - exception, verification fails and the exception is propagated to the - caller of :py:obj:`Connection.do_handshake`. - """ - serverContext = Context(TLSv1_METHOD) - serverContext.use_privatekey( - load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)) - serverContext.use_certificate( - load_certificate(FILETYPE_PEM, cleartextCertificatePEM)) - - clientContext = Context(TLSv1_METHOD) - def verify_callback(*args): - raise Exception("silly verify failure") - clientContext.set_verify(VERIFY_PEER, verify_callback) - - exc = self.assertRaises( - Exception, self._handshake_test, serverContext, clientContext) - self.assertEqual("silly verify failure", str(exc)) - - - def test_add_extra_chain_cert(self): - """ - :py:obj:`Context.add_extra_chain_cert` accepts an :py:obj:`X509` instance to add to - the certificate chain. - - See :py:obj:`_create_certificate_chain` for the details of the certificate - chain tested. - - The chain is tested by starting a server with scert and connecting - to it with a client which trusts cacert and requires verification to - succeed. - """ - chain = _create_certificate_chain() - [(cakey, cacert), (ikey, icert), (skey, scert)] = chain - - # Dump the CA certificate to a file because that's the only way to load - # it as a trusted CA in the client context. - for cert, name in [(cacert, 'ca.pem'), (icert, 'i.pem'), (scert, 's.pem')]: - fObj = open(name, 'w') - fObj.write(dump_certificate(FILETYPE_PEM, cert).decode('ascii')) - fObj.close() - - for key, name in [(cakey, 'ca.key'), (ikey, 'i.key'), (skey, 's.key')]: - fObj = open(name, 'w') - fObj.write(dump_privatekey(FILETYPE_PEM, key).decode('ascii')) - fObj.close() - - # Create the server context - serverContext = Context(TLSv1_METHOD) - serverContext.use_privatekey(skey) - serverContext.use_certificate(scert) - # The client already has cacert, we only need to give them icert. - serverContext.add_extra_chain_cert(icert) - - # Create the client - clientContext = Context(TLSv1_METHOD) - clientContext.set_verify( - VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb) - clientContext.load_verify_locations(b"ca.pem") - - # Try it out. - self._handshake_test(serverContext, clientContext) - - - def test_use_certificate_chain_file(self): - """ - :py:obj:`Context.use_certificate_chain_file` reads a certificate chain from - the specified file. - - The chain is tested by starting a server with scert and connecting - to it with a client which trusts cacert and requires verification to - succeed. - """ - chain = _create_certificate_chain() - [(cakey, cacert), (ikey, icert), (skey, scert)] = chain - - # Write out the chain file. - chainFile = self.mktemp() - fObj = open(chainFile, 'wb') - # Most specific to least general. - fObj.write(dump_certificate(FILETYPE_PEM, scert)) - fObj.write(dump_certificate(FILETYPE_PEM, icert)) - fObj.write(dump_certificate(FILETYPE_PEM, cacert)) - fObj.close() - - serverContext = Context(TLSv1_METHOD) - serverContext.use_certificate_chain_file(chainFile) - serverContext.use_privatekey(skey) - - fObj = open('ca.pem', 'w') - fObj.write(dump_certificate(FILETYPE_PEM, cacert).decode('ascii')) - fObj.close() - - clientContext = Context(TLSv1_METHOD) - clientContext.set_verify( - VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb) - clientContext.load_verify_locations(b"ca.pem") - - self._handshake_test(serverContext, clientContext) - - - def test_use_certificate_chain_file_wrong_args(self): - """ - :py:obj:`Context.use_certificate_chain_file` raises :py:obj:`TypeError` - if passed zero or more than one argument or when passed a non-byte - string single argument. It also raises :py:obj:`OpenSSL.SSL.Error` when - passed a bad chain file name (for example, the name of a file which does - not exist). - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.use_certificate_chain_file) - self.assertRaises(TypeError, context.use_certificate_chain_file, object()) - self.assertRaises(TypeError, context.use_certificate_chain_file, b"foo", object()) - - self.assertRaises(Error, context.use_certificate_chain_file, self.mktemp()) - - # XXX load_client_ca - # XXX set_session_id - - def test_get_verify_mode_wrong_args(self): - """ - :py:obj:`Context.get_verify_mode` raises :py:obj:`TypeError` if called with any - arguments. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.get_verify_mode, None) - - - def test_set_verify_mode(self): - """ - :py:obj:`Context.get_verify_mode` returns the verify mode flags previously - passed to :py:obj:`Context.set_verify`. - """ - context = Context(TLSv1_METHOD) - self.assertEquals(context.get_verify_mode(), 0) - context.set_verify( - VERIFY_PEER | VERIFY_CLIENT_ONCE, lambda *args: None) - self.assertEquals( - context.get_verify_mode(), VERIFY_PEER | VERIFY_CLIENT_ONCE) - - - if not PY3: - def test_set_verify_mode_long(self): - """ - On Python 2 :py:obj:`Context.set_verify_mode` accepts values of - type :py:obj:`long` as well as :py:obj:`int`. - """ - context = Context(TLSv1_METHOD) - self.assertEquals(context.get_verify_mode(), 0) - context.set_verify( - long(VERIFY_PEER | VERIFY_CLIENT_ONCE), lambda *args: None) - self.assertEquals( - context.get_verify_mode(), VERIFY_PEER | VERIFY_CLIENT_ONCE) - - - def test_load_tmp_dh_wrong_args(self): - """ - :py:obj:`Context.load_tmp_dh` raises :py:obj:`TypeError` if called with the wrong - number of arguments or with a non-:py:obj:`str` argument. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.load_tmp_dh) - self.assertRaises(TypeError, context.load_tmp_dh, "foo", None) - self.assertRaises(TypeError, context.load_tmp_dh, object()) - - - def test_load_tmp_dh_missing_file(self): - """ - :py:obj:`Context.load_tmp_dh` raises :py:obj:`OpenSSL.SSL.Error` if the specified file - does not exist. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(Error, context.load_tmp_dh, b"hello") - - - def test_load_tmp_dh(self): - """ - :py:obj:`Context.load_tmp_dh` loads Diffie-Hellman parameters from the - specified file. - """ - context = Context(TLSv1_METHOD) - dhfilename = self.mktemp() - dhfile = open(dhfilename, "w") - dhfile.write(dhparam) - dhfile.close() - context.load_tmp_dh(dhfilename) - # XXX What should I assert here? -exarkun - - - def test_set_tmp_ecdh(self): - """ - :py:obj:`Context.set_tmp_ecdh` sets the elliptic curve for - Diffie-Hellman to the specified curve. - """ - context = Context(TLSv1_METHOD) - for curve in get_elliptic_curves(): - # The only easily "assertable" thing is that it does not raise an - # exception. - context.set_tmp_ecdh(curve) - - - def test_set_cipher_list_bytes(self): - """ - :py:obj:`Context.set_cipher_list` accepts a :py:obj:`bytes` naming the - ciphers which connections created with the context object will be able - to choose from. - """ - context = Context(TLSv1_METHOD) - context.set_cipher_list(b"hello world:EXP-RC4-MD5") - conn = Connection(context, None) - self.assertEquals(conn.get_cipher_list(), ["EXP-RC4-MD5"]) - - - def test_set_cipher_list_text(self): - """ - :py:obj:`Context.set_cipher_list` accepts a :py:obj:`unicode` naming - the ciphers which connections created with the context object will be - able to choose from. - """ - context = Context(TLSv1_METHOD) - context.set_cipher_list(u("hello world:EXP-RC4-MD5")) - conn = Connection(context, None) - self.assertEquals(conn.get_cipher_list(), ["EXP-RC4-MD5"]) - - - def test_set_cipher_list_wrong_args(self): - """ - :py:obj:`Context.set_cipher_list` raises :py:obj:`TypeError` when - passed zero arguments or more than one argument or when passed a - non-string single argument and raises :py:obj:`OpenSSL.SSL.Error` when - passed an incorrect cipher list string. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.set_cipher_list) - self.assertRaises(TypeError, context.set_cipher_list, object()) - self.assertRaises(TypeError, context.set_cipher_list, b"EXP-RC4-MD5", object()) - - self.assertRaises(Error, context.set_cipher_list, "imaginary-cipher") - - - def test_set_session_cache_mode_wrong_args(self): - """ - :py:obj:`Context.set_session_cache_mode` raises :py:obj:`TypeError` if - called with other than one integer argument. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.set_session_cache_mode) - self.assertRaises(TypeError, context.set_session_cache_mode, object()) - - - def test_get_session_cache_mode_wrong_args(self): - """ - :py:obj:`Context.get_session_cache_mode` raises :py:obj:`TypeError` if - called with any arguments. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.get_session_cache_mode, 1) - - - def test_session_cache_mode(self): - """ - :py:obj:`Context.set_session_cache_mode` specifies how sessions are - cached. The setting can be retrieved via - :py:obj:`Context.get_session_cache_mode`. - """ - context = Context(TLSv1_METHOD) - context.set_session_cache_mode(SESS_CACHE_OFF) - off = context.set_session_cache_mode(SESS_CACHE_BOTH) - self.assertEqual(SESS_CACHE_OFF, off) - self.assertEqual(SESS_CACHE_BOTH, context.get_session_cache_mode()) - - if not PY3: - def test_session_cache_mode_long(self): - """ - On Python 2 :py:obj:`Context.set_session_cache_mode` accepts values - of type :py:obj:`long` as well as :py:obj:`int`. - """ - context = Context(TLSv1_METHOD) - context.set_session_cache_mode(long(SESS_CACHE_BOTH)) - self.assertEqual( - SESS_CACHE_BOTH, context.get_session_cache_mode()) - - - def test_get_cert_store(self): - """ - :py:obj:`Context.get_cert_store` returns a :py:obj:`X509Store` instance. - """ - context = Context(TLSv1_METHOD) - store = context.get_cert_store() - self.assertIsInstance(store, X509Store) - - - -class ServerNameCallbackTests(TestCase, _LoopbackMixin): - """ - Tests for :py:obj:`Context.set_tlsext_servername_callback` and its interaction with - :py:obj:`Connection`. - """ - def test_wrong_args(self): - """ - :py:obj:`Context.set_tlsext_servername_callback` raises :py:obj:`TypeError` if called - with other than one argument. - """ - context = Context(TLSv1_METHOD) - self.assertRaises(TypeError, context.set_tlsext_servername_callback) - self.assertRaises( - TypeError, context.set_tlsext_servername_callback, 1, 2) - - - def test_old_callback_forgotten(self): - """ - If :py:obj:`Context.set_tlsext_servername_callback` is used to specify a new - callback, the one it replaces is dereferenced. - """ - def callback(connection): - pass - - def replacement(connection): - pass - - context = Context(TLSv1_METHOD) - context.set_tlsext_servername_callback(callback) - - tracker = ref(callback) - del callback - - context.set_tlsext_servername_callback(replacement) - - # One run of the garbage collector happens to work on CPython. PyPy - # doesn't collect the underlying object until a second run for whatever - # reason. That's fine, it still demonstrates our code has properly - # dropped the reference. - collect() - collect() - - callback = tracker() - if callback is not None: - referrers = get_referrers(callback) - if len(referrers) > 1: - self.fail("Some references remain: %r" % (referrers,)) - - - def test_no_servername(self): - """ - When a client specifies no server name, the callback passed to - :py:obj:`Context.set_tlsext_servername_callback` is invoked and the result of - :py:obj:`Connection.get_servername` is :py:obj:`None`. - """ - args = [] - def servername(conn): - args.append((conn, conn.get_servername())) - context = Context(TLSv1_METHOD) - context.set_tlsext_servername_callback(servername) - - # Lose our reference to it. The Context is responsible for keeping it - # alive now. - del servername - collect() - - # Necessary to actually accept the connection - context.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) - context.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) - - # Do a little connection to trigger the logic - server = Connection(context, None) - server.set_accept_state() - - client = Connection(Context(TLSv1_METHOD), None) - client.set_connect_state() - - self._interactInMemory(server, client) - - self.assertEqual([(server, None)], args) - - - def test_servername(self): - """ - When a client specifies a server name in its hello message, the callback - passed to :py:obj:`Contexts.set_tlsext_servername_callback` is invoked and the - result of :py:obj:`Connection.get_servername` is that server name. - """ - args = [] - def servername(conn): - args.append((conn, conn.get_servername())) - context = Context(TLSv1_METHOD) - context.set_tlsext_servername_callback(servername) - - # Necessary to actually accept the connection - context.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) - context.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) - - # Do a little connection to trigger the logic - server = Connection(context, None) - server.set_accept_state() - - client = Connection(Context(TLSv1_METHOD), None) - client.set_connect_state() - client.set_tlsext_host_name(b("foo1.example.com")) - - self._interactInMemory(server, client) - - self.assertEqual([(server, b("foo1.example.com"))], args) - - - -class SessionTests(TestCase): - """ - Unit tests for :py:obj:`OpenSSL.SSL.Session`. - """ - def test_construction(self): - """ - :py:class:`Session` can be constructed with no arguments, creating a new - instance of that type. - """ - new_session = Session() - self.assertTrue(isinstance(new_session, Session)) - - - def test_construction_wrong_args(self): - """ - If any arguments are passed to :py:class:`Session`, :py:obj:`TypeError` - is raised. - """ - self.assertRaises(TypeError, Session, 123) - self.assertRaises(TypeError, Session, "hello") - self.assertRaises(TypeError, Session, object()) - - - -class ConnectionTests(TestCase, _LoopbackMixin): - """ - Unit tests for :py:obj:`OpenSSL.SSL.Connection`. - """ - # XXX get_peer_certificate -> None - # XXX sock_shutdown - # XXX master_key -> TypeError - # XXX server_random -> TypeError - # XXX state_string - # XXX connect -> TypeError - # XXX connect_ex -> TypeError - # XXX set_connect_state -> TypeError - # XXX set_accept_state -> TypeError - # XXX renegotiate_pending - # XXX do_handshake -> TypeError - # XXX bio_read -> TypeError - # XXX recv -> TypeError - # XXX send -> TypeError - # XXX bio_write -> TypeError - - def test_type(self): - """ - :py:obj:`Connection` and :py:obj:`ConnectionType` refer to the same type object and - can be used to create instances of that type. - """ - self.assertIdentical(Connection, ConnectionType) - ctx = Context(TLSv1_METHOD) - self.assertConsistentType(Connection, 'Connection', ctx, None) - - - def test_get_context(self): - """ - :py:obj:`Connection.get_context` returns the :py:obj:`Context` instance used to - construct the :py:obj:`Connection` instance. - """ - context = Context(TLSv1_METHOD) - connection = Connection(context, None) - self.assertIdentical(connection.get_context(), context) - - - def test_get_context_wrong_args(self): - """ - :py:obj:`Connection.get_context` raises :py:obj:`TypeError` if called with any - arguments. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, connection.get_context, None) - - - def test_set_context_wrong_args(self): - """ - :py:obj:`Connection.set_context` raises :py:obj:`TypeError` if called with a - non-:py:obj:`Context` instance argument or with any number of arguments other - than 1. - """ - ctx = Context(TLSv1_METHOD) - connection = Connection(ctx, None) - self.assertRaises(TypeError, connection.set_context) - self.assertRaises(TypeError, connection.set_context, object()) - self.assertRaises(TypeError, connection.set_context, "hello") - self.assertRaises(TypeError, connection.set_context, 1) - self.assertRaises(TypeError, connection.set_context, 1, 2) - self.assertRaises( - TypeError, connection.set_context, Context(TLSv1_METHOD), 2) - self.assertIdentical(ctx, connection.get_context()) - - - def test_set_context(self): - """ - :py:obj:`Connection.set_context` specifies a new :py:obj:`Context` instance to be used - for the connection. - """ - original = Context(SSLv23_METHOD) - replacement = Context(TLSv1_METHOD) - connection = Connection(original, None) - connection.set_context(replacement) - self.assertIdentical(replacement, connection.get_context()) - # Lose our references to the contexts, just in case the Connection isn't - # properly managing its own contributions to their reference counts. - del original, replacement - collect() - - - def test_set_tlsext_host_name_wrong_args(self): - """ - If :py:obj:`Connection.set_tlsext_host_name` is called with a non-byte string - argument or a byte string with an embedded NUL or other than one - argument, :py:obj:`TypeError` is raised. - """ - conn = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, conn.set_tlsext_host_name) - self.assertRaises(TypeError, conn.set_tlsext_host_name, object()) - self.assertRaises(TypeError, conn.set_tlsext_host_name, 123, 456) - self.assertRaises( - TypeError, conn.set_tlsext_host_name, b("with\0null")) - - if version_info >= (3,): - # On Python 3.x, don't accidentally implicitly convert from text. - self.assertRaises( - TypeError, - conn.set_tlsext_host_name, b("example.com").decode("ascii")) - - - def test_get_servername_wrong_args(self): - """ - :py:obj:`Connection.get_servername` raises :py:obj:`TypeError` if called with any - arguments. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, connection.get_servername, object()) - self.assertRaises(TypeError, connection.get_servername, 1) - self.assertRaises(TypeError, connection.get_servername, "hello") - - - def test_pending(self): - """ - :py:obj:`Connection.pending` returns the number of bytes available for - immediate read. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertEquals(connection.pending(), 0) - - - def test_pending_wrong_args(self): - """ - :py:obj:`Connection.pending` raises :py:obj:`TypeError` if called with any arguments. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, connection.pending, None) - - - def test_connect_wrong_args(self): - """ - :py:obj:`Connection.connect` raises :py:obj:`TypeError` if called with a non-address - argument or with the wrong number of arguments. - """ - connection = Connection(Context(TLSv1_METHOD), socket()) - self.assertRaises(TypeError, connection.connect, None) - self.assertRaises(TypeError, connection.connect) - self.assertRaises(TypeError, connection.connect, ("127.0.0.1", 1), None) - - - def test_connect_refused(self): - """ - :py:obj:`Connection.connect` raises :py:obj:`socket.error` if the underlying socket - connect method raises it. - """ - client = socket() - context = Context(TLSv1_METHOD) - clientSSL = Connection(context, client) - exc = self.assertRaises(error, clientSSL.connect, ("127.0.0.1", 1)) - self.assertEquals(exc.args[0], ECONNREFUSED) - - - def test_connect(self): - """ - :py:obj:`Connection.connect` establishes a connection to the specified address. - """ - port = socket() - port.bind(('', 0)) - port.listen(3) - - clientSSL = Connection(Context(TLSv1_METHOD), socket()) - clientSSL.connect(('127.0.0.1', port.getsockname()[1])) - # XXX An assertion? Or something? - - - if platform == "darwin": - "connect_ex sometimes causes a kernel panic on OS X 10.6.4" - else: - def test_connect_ex(self): - """ - If there is a connection error, :py:obj:`Connection.connect_ex` returns the - errno instead of raising an exception. - """ - port = socket() - port.bind(('', 0)) - port.listen(3) - - clientSSL = Connection(Context(TLSv1_METHOD), socket()) - clientSSL.setblocking(False) - result = clientSSL.connect_ex(port.getsockname()) - expected = (EINPROGRESS, EWOULDBLOCK) - self.assertTrue( - result in expected, "%r not in %r" % (result, expected)) - - - def test_accept_wrong_args(self): - """ - :py:obj:`Connection.accept` raises :py:obj:`TypeError` if called with any arguments. - """ - connection = Connection(Context(TLSv1_METHOD), socket()) - self.assertRaises(TypeError, connection.accept, None) - - - def test_accept(self): - """ - :py:obj:`Connection.accept` accepts a pending connection attempt and returns a - tuple of a new :py:obj:`Connection` (the accepted client) and the address the - connection originated from. - """ - ctx = Context(TLSv1_METHOD) - ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) - ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) - port = socket() - portSSL = Connection(ctx, port) - portSSL.bind(('', 0)) - portSSL.listen(3) - - clientSSL = Connection(Context(TLSv1_METHOD), socket()) - - # Calling portSSL.getsockname() here to get the server IP address sounds - # great, but frequently fails on Windows. - clientSSL.connect(('127.0.0.1', portSSL.getsockname()[1])) - - serverSSL, address = portSSL.accept() - - self.assertTrue(isinstance(serverSSL, Connection)) - self.assertIdentical(serverSSL.get_context(), ctx) - self.assertEquals(address, clientSSL.getsockname()) - - - def test_shutdown_wrong_args(self): - """ - :py:obj:`Connection.shutdown` raises :py:obj:`TypeError` if called with the wrong - number of arguments or with arguments other than integers. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, connection.shutdown, None) - self.assertRaises(TypeError, connection.get_shutdown, None) - self.assertRaises(TypeError, connection.set_shutdown) - self.assertRaises(TypeError, connection.set_shutdown, None) - self.assertRaises(TypeError, connection.set_shutdown, 0, 1) - - - def test_shutdown(self): - """ - :py:obj:`Connection.shutdown` performs an SSL-level connection shutdown. - """ - server, client = self._loopback() - self.assertFalse(server.shutdown()) - self.assertEquals(server.get_shutdown(), SENT_SHUTDOWN) - self.assertRaises(ZeroReturnError, client.recv, 1024) - self.assertEquals(client.get_shutdown(), RECEIVED_SHUTDOWN) - client.shutdown() - self.assertEquals(client.get_shutdown(), SENT_SHUTDOWN|RECEIVED_SHUTDOWN) - self.assertRaises(ZeroReturnError, server.recv, 1024) - self.assertEquals(server.get_shutdown(), SENT_SHUTDOWN|RECEIVED_SHUTDOWN) - - - def test_set_shutdown(self): - """ - :py:obj:`Connection.set_shutdown` sets the state of the SSL connection shutdown - process. - """ - connection = Connection(Context(TLSv1_METHOD), socket()) - connection.set_shutdown(RECEIVED_SHUTDOWN) - self.assertEquals(connection.get_shutdown(), RECEIVED_SHUTDOWN) - - - if not PY3: - def test_set_shutdown_long(self): - """ - On Python 2 :py:obj:`Connection.set_shutdown` accepts an argument - of type :py:obj:`long` as well as :py:obj:`int`. - """ - connection = Connection(Context(TLSv1_METHOD), socket()) - connection.set_shutdown(long(RECEIVED_SHUTDOWN)) - self.assertEquals(connection.get_shutdown(), RECEIVED_SHUTDOWN) - - - def test_app_data_wrong_args(self): - """ - :py:obj:`Connection.set_app_data` raises :py:obj:`TypeError` if called with other than - one argument. :py:obj:`Connection.get_app_data` raises :py:obj:`TypeError` if called - with any arguments. - """ - conn = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, conn.get_app_data, None) - self.assertRaises(TypeError, conn.set_app_data) - self.assertRaises(TypeError, conn.set_app_data, None, None) - - - def test_app_data(self): - """ - Any object can be set as app data by passing it to - :py:obj:`Connection.set_app_data` and later retrieved with - :py:obj:`Connection.get_app_data`. - """ - conn = Connection(Context(TLSv1_METHOD), None) - app_data = object() - conn.set_app_data(app_data) - self.assertIdentical(conn.get_app_data(), app_data) - - - def test_makefile(self): - """ - :py:obj:`Connection.makefile` is not implemented and calling that method raises - :py:obj:`NotImplementedError`. - """ - conn = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(NotImplementedError, conn.makefile) - - - def test_get_peer_cert_chain_wrong_args(self): - """ - :py:obj:`Connection.get_peer_cert_chain` raises :py:obj:`TypeError` if called with any - arguments. - """ - conn = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, conn.get_peer_cert_chain, 1) - self.assertRaises(TypeError, conn.get_peer_cert_chain, "foo") - self.assertRaises(TypeError, conn.get_peer_cert_chain, object()) - self.assertRaises(TypeError, conn.get_peer_cert_chain, []) - - - def test_get_peer_cert_chain(self): - """ - :py:obj:`Connection.get_peer_cert_chain` returns a list of certificates which - the connected server returned for the certification verification. - """ - chain = _create_certificate_chain() - [(cakey, cacert), (ikey, icert), (skey, scert)] = chain - - serverContext = Context(TLSv1_METHOD) - serverContext.use_privatekey(skey) - serverContext.use_certificate(scert) - serverContext.add_extra_chain_cert(icert) - serverContext.add_extra_chain_cert(cacert) - server = Connection(serverContext, None) - server.set_accept_state() - - # Create the client - clientContext = Context(TLSv1_METHOD) - clientContext.set_verify(VERIFY_NONE, verify_cb) - client = Connection(clientContext, None) - client.set_connect_state() - - self._interactInMemory(client, server) - - chain = client.get_peer_cert_chain() - self.assertEqual(len(chain), 3) - self.assertEqual( - "Server Certificate", chain[0].get_subject().CN) - self.assertEqual( - "Intermediate Certificate", chain[1].get_subject().CN) - self.assertEqual( - "Authority Certificate", chain[2].get_subject().CN) - - - def test_get_peer_cert_chain_none(self): - """ - :py:obj:`Connection.get_peer_cert_chain` returns :py:obj:`None` if the peer sends no - certificate chain. - """ - ctx = Context(TLSv1_METHOD) - ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) - ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) - server = Connection(ctx, None) - server.set_accept_state() - client = Connection(Context(TLSv1_METHOD), None) - client.set_connect_state() - self._interactInMemory(client, server) - self.assertIdentical(None, server.get_peer_cert_chain()) - - - def test_get_session_wrong_args(self): - """ - :py:obj:`Connection.get_session` raises :py:obj:`TypeError` if called - with any arguments. - """ - ctx = Context(TLSv1_METHOD) - server = Connection(ctx, None) - self.assertRaises(TypeError, server.get_session, 123) - self.assertRaises(TypeError, server.get_session, "hello") - self.assertRaises(TypeError, server.get_session, object()) - - - def test_get_session_unconnected(self): - """ - :py:obj:`Connection.get_session` returns :py:obj:`None` when used with - an object which has not been connected. - """ - ctx = Context(TLSv1_METHOD) - server = Connection(ctx, None) - session = server.get_session() - self.assertIdentical(None, session) - - - def test_server_get_session(self): - """ - On the server side of a connection, :py:obj:`Connection.get_session` - returns a :py:class:`Session` instance representing the SSL session for - that connection. - """ - server, client = self._loopback() - session = server.get_session() - self.assertIsInstance(session, Session) - - - def test_client_get_session(self): - """ - On the client side of a connection, :py:obj:`Connection.get_session` - returns a :py:class:`Session` instance representing the SSL session for - that connection. - """ - server, client = self._loopback() - session = client.get_session() - self.assertIsInstance(session, Session) - - - def test_set_session_wrong_args(self): - """ - If called with an object that is not an instance of :py:class:`Session`, - or with other than one argument, :py:obj:`Connection.set_session` raises - :py:obj:`TypeError`. - """ - ctx = Context(TLSv1_METHOD) - connection = Connection(ctx, None) - self.assertRaises(TypeError, connection.set_session) - self.assertRaises(TypeError, connection.set_session, 123) - self.assertRaises(TypeError, connection.set_session, "hello") - self.assertRaises(TypeError, connection.set_session, object()) - self.assertRaises( - TypeError, connection.set_session, Session(), Session()) - - - def test_client_set_session(self): - """ - :py:obj:`Connection.set_session`, when used prior to a connection being - established, accepts a :py:class:`Session` instance and causes an - attempt to re-use the session it represents when the SSL handshake is - performed. - """ - key = load_privatekey(FILETYPE_PEM, server_key_pem) - cert = load_certificate(FILETYPE_PEM, server_cert_pem) - ctx = Context(TLSv1_METHOD) - ctx.use_privatekey(key) - ctx.use_certificate(cert) - ctx.set_session_id("unity-test") - - def makeServer(socket): - server = Connection(ctx, socket) - server.set_accept_state() - return server - - originalServer, originalClient = self._loopback( - serverFactory=makeServer) - originalSession = originalClient.get_session() - - def makeClient(socket): - client = self._loopbackClientFactory(socket) - client.set_session(originalSession) - return client - resumedServer, resumedClient = self._loopback( - serverFactory=makeServer, - clientFactory=makeClient) - - # This is a proxy: in general, we have no access to any unique - # identifier for the session (new enough versions of OpenSSL expose a - # hash which could be usable, but "new enough" is very, very new). - # Instead, exploit the fact that the master key is re-used if the - # session is re-used. As long as the master key for the two connections - # is the same, the session was re-used! - self.assertEqual( - originalServer.master_key(), resumedServer.master_key()) - - - def test_set_session_wrong_method(self): - """ - If :py:obj:`Connection.set_session` is passed a :py:class:`Session` - instance associated with a context using a different SSL method than the - :py:obj:`Connection` is using, a :py:class:`OpenSSL.SSL.Error` is - raised. - """ - key = load_privatekey(FILETYPE_PEM, server_key_pem) - cert = load_certificate(FILETYPE_PEM, server_cert_pem) - ctx = Context(TLSv1_METHOD) - ctx.use_privatekey(key) - ctx.use_certificate(cert) - ctx.set_session_id("unity-test") - - def makeServer(socket): - server = Connection(ctx, socket) - server.set_accept_state() - return server - - originalServer, originalClient = self._loopback( - serverFactory=makeServer) - originalSession = originalClient.get_session() - - def makeClient(socket): - # Intentionally use a different, incompatible method here. - client = Connection(Context(SSLv3_METHOD), socket) - client.set_connect_state() - client.set_session(originalSession) - return client - - self.assertRaises( - Error, - self._loopback, clientFactory=makeClient, serverFactory=makeServer) - - - def test_wantWriteError(self): - """ - :py:obj:`Connection` methods which generate output raise - :py:obj:`OpenSSL.SSL.WantWriteError` if writing to the connection's BIO - fail indicating a should-write state. - """ - client_socket, server_socket = socket_pair() - # Fill up the client's send buffer so Connection won't be able to write - # anything. Only write a single byte at a time so we can be sure we - # completely fill the buffer. Even though the socket API is allowed to - # signal a short write via its return value it seems this doesn't - # always happen on all platforms (FreeBSD and OS X particular) for the - # very last bit of available buffer space. - msg = b"x" - for i in range(1024 * 1024 * 4): - try: - client_socket.send(msg) - except error as e: - if e.errno == EWOULDBLOCK: - break - raise - else: - self.fail( - "Failed to fill socket buffer, cannot test BIO want write") - - ctx = Context(TLSv1_METHOD) - conn = Connection(ctx, client_socket) - # Client's speak first, so make it an SSL client - conn.set_connect_state() - self.assertRaises(WantWriteError, conn.do_handshake) - - # XXX want_read - - def test_get_finished_before_connect(self): - """ - :py:obj:`Connection.get_finished` returns :py:obj:`None` before TLS - handshake is completed. - """ - ctx = Context(TLSv1_METHOD) - connection = Connection(ctx, None) - self.assertEqual(connection.get_finished(), None) - - - def test_get_peer_finished_before_connect(self): - """ - :py:obj:`Connection.get_peer_finished` returns :py:obj:`None` before - TLS handshake is completed. - """ - ctx = Context(TLSv1_METHOD) - connection = Connection(ctx, None) - self.assertEqual(connection.get_peer_finished(), None) - - - def test_get_finished(self): - """ - :py:obj:`Connection.get_finished` method returns the TLS Finished - message send from client, or server. Finished messages are send during - TLS handshake. - """ - - server, client = self._loopback() - - self.assertNotEqual(server.get_finished(), None) - self.assertTrue(len(server.get_finished()) > 0) - - - def test_get_peer_finished(self): - """ - :py:obj:`Connection.get_peer_finished` method returns the TLS Finished - message received from client, or server. Finished messages are send - during TLS handshake. - """ - server, client = self._loopback() - - self.assertNotEqual(server.get_peer_finished(), None) - self.assertTrue(len(server.get_peer_finished()) > 0) - - - def test_tls_finished_message_symmetry(self): - """ - The TLS Finished message send by server must be the TLS Finished message - received by client. - - The TLS Finished message send by client must be the TLS Finished message - received by server. - """ - server, client = self._loopback() - - self.assertEqual(server.get_finished(), client.get_peer_finished()) - self.assertEqual(client.get_finished(), server.get_peer_finished()) - - - def test_get_cipher_name_before_connect(self): - """ - :py:obj:`Connection.get_cipher_name` returns :py:obj:`None` if no - connection has been established. - """ - ctx = Context(TLSv1_METHOD) - conn = Connection(ctx, None) - self.assertIdentical(conn.get_cipher_name(), None) - - - def test_get_cipher_name(self): - """ - :py:obj:`Connection.get_cipher_name` returns a :py:class:`unicode` - string giving the name of the currently used cipher. - """ - server, client = self._loopback() - server_cipher_name, client_cipher_name = \ - server.get_cipher_name(), client.get_cipher_name() - - self.assertIsInstance(server_cipher_name, text_type) - self.assertIsInstance(client_cipher_name, text_type) - - self.assertEqual(server_cipher_name, client_cipher_name) - - - def test_get_cipher_version_before_connect(self): - """ - :py:obj:`Connection.get_cipher_version` returns :py:obj:`None` if no - connection has been established. - """ - ctx = Context(TLSv1_METHOD) - conn = Connection(ctx, None) - self.assertIdentical(conn.get_cipher_version(), None) - - - def test_get_cipher_version(self): - """ - :py:obj:`Connection.get_cipher_version` returns a :py:class:`unicode` - string giving the protocol name of the currently used cipher. - """ - server, client = self._loopback() - server_cipher_version, client_cipher_version = \ - server.get_cipher_version(), client.get_cipher_version() - - self.assertIsInstance(server_cipher_version, text_type) - self.assertIsInstance(client_cipher_version, text_type) - - self.assertEqual(server_cipher_version, client_cipher_version) - - - def test_get_cipher_bits_before_connect(self): - """ - :py:obj:`Connection.get_cipher_bits` returns :py:obj:`None` if no - connection has been established. - """ - ctx = Context(TLSv1_METHOD) - conn = Connection(ctx, None) - self.assertIdentical(conn.get_cipher_bits(), None) - - - def test_get_cipher_bits(self): - """ - :py:obj:`Connection.get_cipher_bits` returns the number of secret bits - of the currently used cipher. - """ - server, client = self._loopback() - server_cipher_bits, client_cipher_bits = \ - server.get_cipher_bits(), client.get_cipher_bits() - - self.assertIsInstance(server_cipher_bits, int) - self.assertIsInstance(client_cipher_bits, int) - - self.assertEqual(server_cipher_bits, client_cipher_bits) - - - -class ConnectionGetCipherListTests(TestCase): - """ - Tests for :py:obj:`Connection.get_cipher_list`. - """ - def test_wrong_args(self): - """ - :py:obj:`Connection.get_cipher_list` raises :py:obj:`TypeError` if called with any - arguments. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, connection.get_cipher_list, None) - - - def test_result(self): - """ - :py:obj:`Connection.get_cipher_list` returns a :py:obj:`list` of - :py:obj:`bytes` giving the names of the ciphers which might be used. - """ - connection = Connection(Context(TLSv1_METHOD), None) - ciphers = connection.get_cipher_list() - self.assertTrue(isinstance(ciphers, list)) - for cipher in ciphers: - self.assertTrue(isinstance(cipher, str)) - - - -class ConnectionSendTests(TestCase, _LoopbackMixin): - """ - Tests for :py:obj:`Connection.send` - """ - def test_wrong_args(self): - """ - When called with arguments other than string argument for its first - parameter or more than two arguments, :py:obj:`Connection.send` raises - :py:obj:`TypeError`. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, connection.send) - self.assertRaises(TypeError, connection.send, object()) - self.assertRaises(TypeError, connection.send, "foo", object(), "bar") - - - def test_short_bytes(self): - """ - When passed a short byte string, :py:obj:`Connection.send` transmits all of it - and returns the number of bytes sent. - """ - server, client = self._loopback() - count = server.send(b('xy')) - self.assertEquals(count, 2) - self.assertEquals(client.recv(2), b('xy')) - - try: - memoryview - except NameError: - "cannot test sending memoryview without memoryview" - else: - def test_short_memoryview(self): - """ - When passed a memoryview onto a small number of bytes, - :py:obj:`Connection.send` transmits all of them and returns the number of - bytes sent. - """ - server, client = self._loopback() - count = server.send(memoryview(b('xy'))) - self.assertEquals(count, 2) - self.assertEquals(client.recv(2), b('xy')) - - - try: - buffer - except NameError: - "cannot test sending buffer without buffer" - else: - def test_short_buffer(self): - """ - When passed a buffer containing a small number of bytes, - :py:obj:`Connection.send` transmits all of them and returns the number of - bytes sent. - """ - server, client = self._loopback() - count = server.send(buffer(b('xy'))) - self.assertEquals(count, 2) - self.assertEquals(client.recv(2), b('xy')) - - - -class ConnectionSendallTests(TestCase, _LoopbackMixin): - """ - Tests for :py:obj:`Connection.sendall`. - """ - def test_wrong_args(self): - """ - When called with arguments other than a string argument for its first - parameter or with more than two arguments, :py:obj:`Connection.sendall` - raises :py:obj:`TypeError`. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, connection.sendall) - self.assertRaises(TypeError, connection.sendall, object()) - self.assertRaises( - TypeError, connection.sendall, "foo", object(), "bar") - - - def test_short(self): - """ - :py:obj:`Connection.sendall` transmits all of the bytes in the string passed to - it. - """ - server, client = self._loopback() - server.sendall(b('x')) - self.assertEquals(client.recv(1), b('x')) - - - try: - memoryview - except NameError: - "cannot test sending memoryview without memoryview" - else: - def test_short_memoryview(self): - """ - When passed a memoryview onto a small number of bytes, - :py:obj:`Connection.sendall` transmits all of them. - """ - server, client = self._loopback() - server.sendall(memoryview(b('x'))) - self.assertEquals(client.recv(1), b('x')) - - - try: - buffer - except NameError: - "cannot test sending buffers without buffers" - else: - def test_short_buffers(self): - """ - When passed a buffer containing a small number of bytes, - :py:obj:`Connection.sendall` transmits all of them. - """ - server, client = self._loopback() - server.sendall(buffer(b('x'))) - self.assertEquals(client.recv(1), b('x')) - - - def test_long(self): - """ - :py:obj:`Connection.sendall` transmits all of the bytes in the string passed to - it even if this requires multiple calls of an underlying write function. - """ - server, client = self._loopback() - # Should be enough, underlying SSL_write should only do 16k at a time. - # On Windows, after 32k of bytes the write will block (forever - because - # no one is yet reading). - message = b('x') * (1024 * 32 - 1) + b('y') - server.sendall(message) - accum = [] - received = 0 - while received < len(message): - data = client.recv(1024) - accum.append(data) - received += len(data) - self.assertEquals(message, b('').join(accum)) - - - def test_closed(self): - """ - If the underlying socket is closed, :py:obj:`Connection.sendall` propagates the - write error from the low level write call. - """ - server, client = self._loopback() - server.sock_shutdown(2) - exc = self.assertRaises(SysCallError, server.sendall, b"hello, world") - if platform == "win32": - self.assertEqual(exc.args[0], ESHUTDOWN) - else: - self.assertEqual(exc.args[0], EPIPE) - - - -class ConnectionRenegotiateTests(TestCase, _LoopbackMixin): - """ - Tests for SSL renegotiation APIs. - """ - def test_renegotiate_wrong_args(self): - """ - :py:obj:`Connection.renegotiate` raises :py:obj:`TypeError` if called with any - arguments. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, connection.renegotiate, None) - - - def test_total_renegotiations_wrong_args(self): - """ - :py:obj:`Connection.total_renegotiations` raises :py:obj:`TypeError` if called with - any arguments. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertRaises(TypeError, connection.total_renegotiations, None) - - - def test_total_renegotiations(self): - """ - :py:obj:`Connection.total_renegotiations` returns :py:obj:`0` before any - renegotiations have happened. - """ - connection = Connection(Context(TLSv1_METHOD), None) - self.assertEquals(connection.total_renegotiations(), 0) - - -# def test_renegotiate(self): -# """ -# """ -# server, client = self._loopback() - -# server.send("hello world") -# self.assertEquals(client.recv(len("hello world")), "hello world") - -# self.assertEquals(server.total_renegotiations(), 0) -# self.assertTrue(server.renegotiate()) - -# server.setblocking(False) -# client.setblocking(False) -# while server.renegotiate_pending(): -# client.do_handshake() -# server.do_handshake() - -# self.assertEquals(server.total_renegotiations(), 1) - - - - -class ErrorTests(TestCase): - """ - Unit tests for :py:obj:`OpenSSL.SSL.Error`. - """ - def test_type(self): - """ - :py:obj:`Error` is an exception type. - """ - self.assertTrue(issubclass(Error, Exception)) - self.assertEqual(Error.__name__, 'Error') - - - -class ConstantsTests(TestCase): - """ - Tests for the values of constants exposed in :py:obj:`OpenSSL.SSL`. - - These are values defined by OpenSSL intended only to be used as flags to - OpenSSL APIs. The only assertions it seems can be made about them is - their values. - """ - # unittest.TestCase has no skip mechanism - if OP_NO_QUERY_MTU is not None: - def test_op_no_query_mtu(self): - """ - The value of :py:obj:`OpenSSL.SSL.OP_NO_QUERY_MTU` is 0x1000, the value of - :py:const:`SSL_OP_NO_QUERY_MTU` defined by :file:`openssl/ssl.h`. - """ - self.assertEqual(OP_NO_QUERY_MTU, 0x1000) - else: - "OP_NO_QUERY_MTU unavailable - OpenSSL version may be too old" - - - if OP_COOKIE_EXCHANGE is not None: - def test_op_cookie_exchange(self): - """ - The value of :py:obj:`OpenSSL.SSL.OP_COOKIE_EXCHANGE` is 0x2000, the value - of :py:const:`SSL_OP_COOKIE_EXCHANGE` defined by :file:`openssl/ssl.h`. - """ - self.assertEqual(OP_COOKIE_EXCHANGE, 0x2000) - else: - "OP_COOKIE_EXCHANGE unavailable - OpenSSL version may be too old" - - - if OP_NO_TICKET is not None: - def test_op_no_ticket(self): - """ - The value of :py:obj:`OpenSSL.SSL.OP_NO_TICKET` is 0x4000, the value of - :py:const:`SSL_OP_NO_TICKET` defined by :file:`openssl/ssl.h`. - """ - self.assertEqual(OP_NO_TICKET, 0x4000) - else: - "OP_NO_TICKET unavailable - OpenSSL version may be too old" - - - if OP_NO_COMPRESSION is not None: - def test_op_no_compression(self): - """ - The value of :py:obj:`OpenSSL.SSL.OP_NO_COMPRESSION` is 0x20000, the value - of :py:const:`SSL_OP_NO_COMPRESSION` defined by :file:`openssl/ssl.h`. - """ - self.assertEqual(OP_NO_COMPRESSION, 0x20000) - else: - "OP_NO_COMPRESSION unavailable - OpenSSL version may be too old" - - - def test_sess_cache_off(self): - """ - The value of :py:obj:`OpenSSL.SSL.SESS_CACHE_OFF` 0x0, the value of - :py:obj:`SSL_SESS_CACHE_OFF` defined by ``openssl/ssl.h``. - """ - self.assertEqual(0x0, SESS_CACHE_OFF) - - - def test_sess_cache_client(self): - """ - The value of :py:obj:`OpenSSL.SSL.SESS_CACHE_CLIENT` 0x1, the value of - :py:obj:`SSL_SESS_CACHE_CLIENT` defined by ``openssl/ssl.h``. - """ - self.assertEqual(0x1, SESS_CACHE_CLIENT) - - - def test_sess_cache_server(self): - """ - The value of :py:obj:`OpenSSL.SSL.SESS_CACHE_SERVER` 0x2, the value of - :py:obj:`SSL_SESS_CACHE_SERVER` defined by ``openssl/ssl.h``. - """ - self.assertEqual(0x2, SESS_CACHE_SERVER) - - - def test_sess_cache_both(self): - """ - The value of :py:obj:`OpenSSL.SSL.SESS_CACHE_BOTH` 0x3, the value of - :py:obj:`SSL_SESS_CACHE_BOTH` defined by ``openssl/ssl.h``. - """ - self.assertEqual(0x3, SESS_CACHE_BOTH) - - - def test_sess_cache_no_auto_clear(self): - """ - The value of :py:obj:`OpenSSL.SSL.SESS_CACHE_NO_AUTO_CLEAR` 0x80, the - value of :py:obj:`SSL_SESS_CACHE_NO_AUTO_CLEAR` defined by - ``openssl/ssl.h``. - """ - self.assertEqual(0x80, SESS_CACHE_NO_AUTO_CLEAR) - - - def test_sess_cache_no_internal_lookup(self): - """ - The value of :py:obj:`OpenSSL.SSL.SESS_CACHE_NO_INTERNAL_LOOKUP` 0x100, - the value of :py:obj:`SSL_SESS_CACHE_NO_INTERNAL_LOOKUP` defined by - ``openssl/ssl.h``. - """ - self.assertEqual(0x100, SESS_CACHE_NO_INTERNAL_LOOKUP) - - - def test_sess_cache_no_internal_store(self): - """ - The value of :py:obj:`OpenSSL.SSL.SESS_CACHE_NO_INTERNAL_STORE` 0x200, - the value of :py:obj:`SSL_SESS_CACHE_NO_INTERNAL_STORE` defined by - ``openssl/ssl.h``. - """ - self.assertEqual(0x200, SESS_CACHE_NO_INTERNAL_STORE) - - - def test_sess_cache_no_internal(self): - """ - The value of :py:obj:`OpenSSL.SSL.SESS_CACHE_NO_INTERNAL` 0x300, the - value of :py:obj:`SSL_SESS_CACHE_NO_INTERNAL` defined by - ``openssl/ssl.h``. - """ - self.assertEqual(0x300, SESS_CACHE_NO_INTERNAL) - - - -class MemoryBIOTests(TestCase, _LoopbackMixin): - """ - Tests for :py:obj:`OpenSSL.SSL.Connection` using a memory BIO. - """ - def _server(self, sock): - """ - Create a new server-side SSL :py:obj:`Connection` object wrapped around - :py:obj:`sock`. - """ - # Create the server side Connection. This is mostly setup boilerplate - # - use TLSv1, use a particular certificate, etc. - server_ctx = Context(TLSv1_METHOD) - server_ctx.set_options(OP_NO_SSLv2 | OP_NO_SSLv3 | OP_SINGLE_DH_USE ) - server_ctx.set_verify(VERIFY_PEER|VERIFY_FAIL_IF_NO_PEER_CERT|VERIFY_CLIENT_ONCE, verify_cb) - server_store = server_ctx.get_cert_store() - server_ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) - server_ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) - server_ctx.check_privatekey() - server_store.add_cert(load_certificate(FILETYPE_PEM, root_cert_pem)) - # Here the Connection is actually created. If None is passed as the 2nd - # parameter, it indicates a memory BIO should be created. - server_conn = Connection(server_ctx, sock) - server_conn.set_accept_state() - return server_conn - - - def _client(self, sock): - """ - Create a new client-side SSL :py:obj:`Connection` object wrapped around - :py:obj:`sock`. - """ - # Now create the client side Connection. Similar boilerplate to the - # above. - client_ctx = Context(TLSv1_METHOD) - client_ctx.set_options(OP_NO_SSLv2 | OP_NO_SSLv3 | OP_SINGLE_DH_USE ) - client_ctx.set_verify(VERIFY_PEER|VERIFY_FAIL_IF_NO_PEER_CERT|VERIFY_CLIENT_ONCE, verify_cb) - client_store = client_ctx.get_cert_store() - client_ctx.use_privatekey(load_privatekey(FILETYPE_PEM, client_key_pem)) - client_ctx.use_certificate(load_certificate(FILETYPE_PEM, client_cert_pem)) - client_ctx.check_privatekey() - client_store.add_cert(load_certificate(FILETYPE_PEM, root_cert_pem)) - client_conn = Connection(client_ctx, sock) - client_conn.set_connect_state() - return client_conn - - - def test_memoryConnect(self): - """ - Two :py:obj:`Connection`s which use memory BIOs can be manually connected by - reading from the output of each and writing those bytes to the input of - the other and in this way establish a connection and exchange - application-level bytes with each other. - """ - server_conn = self._server(None) - client_conn = self._client(None) - - # There should be no key or nonces yet. - self.assertIdentical(server_conn.master_key(), None) - self.assertIdentical(server_conn.client_random(), None) - self.assertIdentical(server_conn.server_random(), None) - - # First, the handshake needs to happen. We'll deliver bytes back and - # forth between the client and server until neither of them feels like - # speaking any more. - self.assertIdentical( - self._interactInMemory(client_conn, server_conn), None) - - # Now that the handshake is done, there should be a key and nonces. - self.assertNotIdentical(server_conn.master_key(), None) - self.assertNotIdentical(server_conn.client_random(), None) - self.assertNotIdentical(server_conn.server_random(), None) - self.assertEquals(server_conn.client_random(), client_conn.client_random()) - self.assertEquals(server_conn.server_random(), client_conn.server_random()) - self.assertNotEquals(server_conn.client_random(), server_conn.server_random()) - self.assertNotEquals(client_conn.client_random(), client_conn.server_random()) - - # Here are the bytes we'll try to send. - important_message = b('One if by land, two if by sea.') - - server_conn.write(important_message) - self.assertEquals( - self._interactInMemory(client_conn, server_conn), - (client_conn, important_message)) - - client_conn.write(important_message[::-1]) - self.assertEquals( - self._interactInMemory(client_conn, server_conn), - (server_conn, important_message[::-1])) - - - def test_socketConnect(self): - """ - Just like :py:obj:`test_memoryConnect` but with an actual socket. - - This is primarily to rule out the memory BIO code as the source of - any problems encountered while passing data over a :py:obj:`Connection` (if - this test fails, there must be a problem outside the memory BIO - code, as no memory BIO is involved here). Even though this isn't a - memory BIO test, it's convenient to have it here. - """ - server_conn, client_conn = self._loopback() - - important_message = b("Help me Obi Wan Kenobi, you're my only hope.") - client_conn.send(important_message) - msg = server_conn.recv(1024) - self.assertEqual(msg, important_message) - - # Again in the other direction, just for fun. - important_message = important_message[::-1] - server_conn.send(important_message) - msg = client_conn.recv(1024) - self.assertEqual(msg, important_message) - - - def test_socketOverridesMemory(self): - """ - Test that :py:obj:`OpenSSL.SSL.bio_read` and :py:obj:`OpenSSL.SSL.bio_write` don't - work on :py:obj:`OpenSSL.SSL.Connection`() that use sockets. - """ - context = Context(SSLv3_METHOD) - client = socket() - clientSSL = Connection(context, client) - self.assertRaises( TypeError, clientSSL.bio_read, 100) - self.assertRaises( TypeError, clientSSL.bio_write, "foo") - self.assertRaises( TypeError, clientSSL.bio_shutdown ) - - - def test_outgoingOverflow(self): - """ - If more bytes than can be written to the memory BIO are passed to - :py:obj:`Connection.send` at once, the number of bytes which were written is - returned and that many bytes from the beginning of the input can be - read from the other end of the connection. - """ - server = self._server(None) - client = self._client(None) - - self._interactInMemory(client, server) - - size = 2 ** 15 - sent = client.send(b"x" * size) - # Sanity check. We're trying to test what happens when the entire - # input can't be sent. If the entire input was sent, this test is - # meaningless. - self.assertTrue(sent < size) - - receiver, received = self._interactInMemory(client, server) - self.assertIdentical(receiver, server) - - # We can rely on all of these bytes being received at once because - # _loopback passes 2 ** 16 to recv - more than 2 ** 15. - self.assertEquals(len(received), sent) - - - def test_shutdown(self): - """ - :py:obj:`Connection.bio_shutdown` signals the end of the data stream from - which the :py:obj:`Connection` reads. - """ - server = self._server(None) - server.bio_shutdown() - e = self.assertRaises(Error, server.recv, 1024) - # We don't want WantReadError or ZeroReturnError or anything - it's a - # handshake failure. - self.assertEquals(e.__class__, Error) - - - def test_unexpectedEndOfFile(self): - """ - If the connection is lost before an orderly SSL shutdown occurs, - :py:obj:`OpenSSL.SSL.SysCallError` is raised with a message of - "Unexpected EOF". - """ - server_conn, client_conn = self._loopback() - client_conn.sock_shutdown(SHUT_RDWR) - exc = self.assertRaises(SysCallError, server_conn.recv, 1024) - self.assertEqual(exc.args, (-1, "Unexpected EOF")) - - - def _check_client_ca_list(self, func): - """ - Verify the return value of the :py:obj:`get_client_ca_list` method for server and client connections. - - :param func: A function which will be called with the server context - before the client and server are connected to each other. This - function should specify a list of CAs for the server to send to the - client and return that same list. The list will be used to verify - that :py:obj:`get_client_ca_list` returns the proper value at various - times. - """ - server = self._server(None) - client = self._client(None) - self.assertEqual(client.get_client_ca_list(), []) - self.assertEqual(server.get_client_ca_list(), []) - ctx = server.get_context() - expected = func(ctx) - self.assertEqual(client.get_client_ca_list(), []) - self.assertEqual(server.get_client_ca_list(), expected) - self._interactInMemory(client, server) - self.assertEqual(client.get_client_ca_list(), expected) - self.assertEqual(server.get_client_ca_list(), expected) - - - def test_set_client_ca_list_errors(self): - """ - :py:obj:`Context.set_client_ca_list` raises a :py:obj:`TypeError` if called with a - non-list or a list that contains objects other than X509Names. - """ - ctx = Context(TLSv1_METHOD) - self.assertRaises(TypeError, ctx.set_client_ca_list, "spam") - self.assertRaises(TypeError, ctx.set_client_ca_list, ["spam"]) - self.assertIdentical(ctx.set_client_ca_list([]), None) - - - def test_set_empty_ca_list(self): - """ - If passed an empty list, :py:obj:`Context.set_client_ca_list` configures the - context to send no CA names to the client and, on both the server and - client sides, :py:obj:`Connection.get_client_ca_list` returns an empty list - after the connection is set up. - """ - def no_ca(ctx): - ctx.set_client_ca_list([]) - return [] - self._check_client_ca_list(no_ca) - - - def test_set_one_ca_list(self): - """ - If passed a list containing a single X509Name, - :py:obj:`Context.set_client_ca_list` configures the context to send that CA - name to the client and, on both the server and client sides, - :py:obj:`Connection.get_client_ca_list` returns a list containing that - X509Name after the connection is set up. - """ - cacert = load_certificate(FILETYPE_PEM, root_cert_pem) - cadesc = cacert.get_subject() - def single_ca(ctx): - ctx.set_client_ca_list([cadesc]) - return [cadesc] - self._check_client_ca_list(single_ca) - - - def test_set_multiple_ca_list(self): - """ - If passed a list containing multiple X509Name objects, - :py:obj:`Context.set_client_ca_list` configures the context to send those CA - names to the client and, on both the server and client sides, - :py:obj:`Connection.get_client_ca_list` returns a list containing those - X509Names after the connection is set up. - """ - secert = load_certificate(FILETYPE_PEM, server_cert_pem) - clcert = load_certificate(FILETYPE_PEM, server_cert_pem) - - sedesc = secert.get_subject() - cldesc = clcert.get_subject() - - def multiple_ca(ctx): - L = [sedesc, cldesc] - ctx.set_client_ca_list(L) - return L - self._check_client_ca_list(multiple_ca) - - - def test_reset_ca_list(self): - """ - If called multiple times, only the X509Names passed to the final call - of :py:obj:`Context.set_client_ca_list` are used to configure the CA names - sent to the client. - """ - cacert = load_certificate(FILETYPE_PEM, root_cert_pem) - secert = load_certificate(FILETYPE_PEM, server_cert_pem) - clcert = load_certificate(FILETYPE_PEM, server_cert_pem) - - cadesc = cacert.get_subject() - sedesc = secert.get_subject() - cldesc = clcert.get_subject() - - def changed_ca(ctx): - ctx.set_client_ca_list([sedesc, cldesc]) - ctx.set_client_ca_list([cadesc]) - return [cadesc] - self._check_client_ca_list(changed_ca) - - - def test_mutated_ca_list(self): - """ - If the list passed to :py:obj:`Context.set_client_ca_list` is mutated - afterwards, this does not affect the list of CA names sent to the - client. - """ - cacert = load_certificate(FILETYPE_PEM, root_cert_pem) - secert = load_certificate(FILETYPE_PEM, server_cert_pem) - - cadesc = cacert.get_subject() - sedesc = secert.get_subject() - - def mutated_ca(ctx): - L = [cadesc] - ctx.set_client_ca_list([cadesc]) - L.append(sedesc) - return [cadesc] - self._check_client_ca_list(mutated_ca) - - - def test_add_client_ca_errors(self): - """ - :py:obj:`Context.add_client_ca` raises :py:obj:`TypeError` if called with a non-X509 - object or with a number of arguments other than one. - """ - ctx = Context(TLSv1_METHOD) - cacert = load_certificate(FILETYPE_PEM, root_cert_pem) - self.assertRaises(TypeError, ctx.add_client_ca) - self.assertRaises(TypeError, ctx.add_client_ca, "spam") - self.assertRaises(TypeError, ctx.add_client_ca, cacert, cacert) - - - def test_one_add_client_ca(self): - """ - A certificate's subject can be added as a CA to be sent to the client - with :py:obj:`Context.add_client_ca`. - """ - cacert = load_certificate(FILETYPE_PEM, root_cert_pem) - cadesc = cacert.get_subject() - def single_ca(ctx): - ctx.add_client_ca(cacert) - return [cadesc] - self._check_client_ca_list(single_ca) - - - def test_multiple_add_client_ca(self): - """ - Multiple CA names can be sent to the client by calling - :py:obj:`Context.add_client_ca` with multiple X509 objects. - """ - cacert = load_certificate(FILETYPE_PEM, root_cert_pem) - secert = load_certificate(FILETYPE_PEM, server_cert_pem) - - cadesc = cacert.get_subject() - sedesc = secert.get_subject() - - def multiple_ca(ctx): - ctx.add_client_ca(cacert) - ctx.add_client_ca(secert) - return [cadesc, sedesc] - self._check_client_ca_list(multiple_ca) - - - def test_set_and_add_client_ca(self): - """ - A call to :py:obj:`Context.set_client_ca_list` followed by a call to - :py:obj:`Context.add_client_ca` results in using the CA names from the first - call and the CA name from the second call. - """ - cacert = load_certificate(FILETYPE_PEM, root_cert_pem) - secert = load_certificate(FILETYPE_PEM, server_cert_pem) - clcert = load_certificate(FILETYPE_PEM, server_cert_pem) - - cadesc = cacert.get_subject() - sedesc = secert.get_subject() - cldesc = clcert.get_subject() - - def mixed_set_add_ca(ctx): - ctx.set_client_ca_list([cadesc, sedesc]) - ctx.add_client_ca(clcert) - return [cadesc, sedesc, cldesc] - self._check_client_ca_list(mixed_set_add_ca) - - - def test_set_after_add_client_ca(self): - """ - A call to :py:obj:`Context.set_client_ca_list` after a call to - :py:obj:`Context.add_client_ca` replaces the CA name specified by the former - call with the names specified by the latter cal. - """ - cacert = load_certificate(FILETYPE_PEM, root_cert_pem) - secert = load_certificate(FILETYPE_PEM, server_cert_pem) - clcert = load_certificate(FILETYPE_PEM, server_cert_pem) - - cadesc = cacert.get_subject() - sedesc = secert.get_subject() - - def set_replaces_add_ca(ctx): - ctx.add_client_ca(clcert) - ctx.set_client_ca_list([cadesc]) - ctx.add_client_ca(secert) - return [cadesc, sedesc] - self._check_client_ca_list(set_replaces_add_ca) - - - -class ConnectionBIOTests(TestCase): - """ - Tests for :py:obj:`Connection.bio_read` and :py:obj:`Connection.bio_write`. - """ - def test_wantReadError(self): - """ - :py:obj:`Connection.bio_read` raises :py:obj:`OpenSSL.SSL.WantReadError` - if there are no bytes available to be read from the BIO. - """ - ctx = Context(TLSv1_METHOD) - conn = Connection(ctx, None) - self.assertRaises(WantReadError, conn.bio_read, 1024) - - - def test_buffer_size(self): - """ - :py:obj:`Connection.bio_read` accepts an integer giving the maximum - number of bytes to read and return. - """ - ctx = Context(TLSv1_METHOD) - conn = Connection(ctx, None) - conn.set_connect_state() - try: - conn.do_handshake() - except WantReadError: - pass - data = conn.bio_read(2) - self.assertEqual(2, len(data)) - - - if not PY3: - def test_buffer_size_long(self): - """ - On Python 2 :py:obj:`Connection.bio_read` accepts values of type - :py:obj:`long` as well as :py:obj:`int`. - """ - ctx = Context(TLSv1_METHOD) - conn = Connection(ctx, None) - conn.set_connect_state() - try: - conn.do_handshake() - except WantReadError: - pass - data = conn.bio_read(long(2)) - self.assertEqual(2, len(data)) - - - - -class InfoConstantTests(TestCase): - """ - Tests for assorted constants exposed for use in info callbacks. - """ - def test_integers(self): - """ - All of the info constants are integers. - - This is a very weak test. It would be nice to have one that actually - verifies that as certain info events happen, the value passed to the - info callback matches up with the constant exposed by OpenSSL.SSL. - """ - for const in [ - SSL_ST_CONNECT, SSL_ST_ACCEPT, SSL_ST_MASK, SSL_ST_INIT, - SSL_ST_BEFORE, SSL_ST_OK, SSL_ST_RENEGOTIATE, - SSL_CB_LOOP, SSL_CB_EXIT, SSL_CB_READ, SSL_CB_WRITE, SSL_CB_ALERT, - SSL_CB_READ_ALERT, SSL_CB_WRITE_ALERT, SSL_CB_ACCEPT_LOOP, - SSL_CB_ACCEPT_EXIT, SSL_CB_CONNECT_LOOP, SSL_CB_CONNECT_EXIT, - SSL_CB_HANDSHAKE_START, SSL_CB_HANDSHAKE_DONE]: - - self.assertTrue(isinstance(const, int)) - - -if __name__ == '__main__': - main() diff --git a/OpenSSL/test/test_tsafe.py b/OpenSSL/test/test_tsafe.py deleted file mode 100644 index 04569574d..000000000 --- a/OpenSSL/test/test_tsafe.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. - -""" -Unit tests for :py:obj:`OpenSSL.tsafe`. -""" - -from OpenSSL.SSL import TLSv1_METHOD, Context -from OpenSSL.tsafe import Connection -from OpenSSL.test.util import TestCase - - -class ConnectionTest(TestCase): - """ - Tests for :py:obj:`OpenSSL.tsafe.Connection`. - """ - def test_instantiation(self): - """ - :py:obj:`OpenSSL.tsafe.Connection` can be instantiated. - """ - # The following line should not throw an error. This isn't an ideal - # test. It would be great to refactor the other Connection tests so - # they could automatically be applied to this class too. - Connection(Context(TLSv1_METHOD), None) diff --git a/OpenSSL/test/util.py b/OpenSSL/test/util.py deleted file mode 100644 index 21bbdc45f..000000000 --- a/OpenSSL/test/util.py +++ /dev/null @@ -1,449 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# Copyright (C) Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -Helpers for the OpenSSL test suite, largely copied from -U{Twisted}. -""" - -import shutil -import traceback -import os, os.path -from tempfile import mktemp -from unittest import TestCase -import sys - -from OpenSSL._util import exception_from_error_queue -from OpenSSL.crypto import Error - -try: - import memdbg -except Exception: - class _memdbg(object): heap = None - memdbg = _memdbg() - -from OpenSSL._util import ffi, lib, byte_string as b - -class TestCase(TestCase): - """ - :py:class:`TestCase` adds useful testing functionality beyond what is available - from the standard library :py:class:`unittest.TestCase`. - """ - def run(self, result): - run = super(TestCase, self).run - if memdbg.heap is None: - return run(result) - - # Run the test as usual - before = set(memdbg.heap) - run(result) - - # Clean up some long-lived allocations so they won't be reported as - # memory leaks. - lib.CRYPTO_cleanup_all_ex_data() - lib.ERR_remove_thread_state(ffi.NULL) - after = set(memdbg.heap) - - if not after - before: - # No leaks, fast succeed - return - - if result.wasSuccessful(): - # If it passed, run it again with memory debugging - before = set(memdbg.heap) - run(result) - - # Clean up some long-lived allocations so they won't be reported as - # memory leaks. - lib.CRYPTO_cleanup_all_ex_data() - lib.ERR_remove_thread_state(ffi.NULL) - - after = set(memdbg.heap) - - self._reportLeaks(after - before, result) - - - def _reportLeaks(self, leaks, result): - def format_leak(p): - stacks = memdbg.heap[p] - # Eventually look at multiple stacks for the realloc() case. For - # now just look at the original allocation location. - (size, python_stack, c_stack) = stacks[0] - - stack = traceback.format_list(python_stack)[:-1] - - # c_stack looks something like this (interesting parts indicated - # with inserted arrows not part of the data): - # - # /home/exarkun/Projects/pyOpenSSL/branches/use-opentls/__pycache__/_cffi__x89095113xb9185b9b.so(+0x12cf) [0x7fe2e20582cf] - # /home/exarkun/Projects/cpython/2.7/python(PyCFunction_Call+0x8b) [0x56265a] - # /home/exarkun/Projects/cpython/2.7/python() [0x4d5f52] - # /home/exarkun/Projects/cpython/2.7/python(PyEval_EvalFrameEx+0x753b) [0x4d0e1e] - # /home/exarkun/Projects/cpython/2.7/python() [0x4d6419] - # /home/exarkun/Projects/cpython/2.7/python() [0x4d6129] - # /home/exarkun/Projects/cpython/2.7/python(PyEval_EvalFrameEx+0x753b) [0x4d0e1e] - # /home/exarkun/Projects/cpython/2.7/python(PyEval_EvalCodeEx+0x1043) [0x4d3726] - # /home/exarkun/Projects/cpython/2.7/python() [0x55fd51] - # /home/exarkun/Projects/cpython/2.7/python(PyObject_Call+0x7e) [0x420ee6] - # /home/exarkun/Projects/cpython/2.7/python(PyEval_CallObjectWithKeywords+0x158) [0x4d56ec] - # /home/exarkun/.local/lib/python2.7/site-packages/cffi-0.5-py2.7-linux-x86_64.egg/_cffi_backend.so(+0xe96e) [0x7fe2e38be96e] - # /usr/lib/x86_64-linux-gnu/libffi.so.6(ffi_closure_unix64_inner+0x1b9) [0x7fe2e36ad819] - # /usr/lib/x86_64-linux-gnu/libffi.so.6(ffi_closure_unix64+0x46) [0x7fe2e36adb7c] - # /lib/x86_64-linux-gnu/libcrypto.so.1.0.0(CRYPTO_malloc+0x64) [0x7fe2e1cef784] <------ end interesting - # /lib/x86_64-linux-gnu/libcrypto.so.1.0.0(lh_insert+0x16b) [0x7fe2e1d6a24b] . - # /lib/x86_64-linux-gnu/libcrypto.so.1.0.0(+0x61c18) [0x7fe2e1cf0c18] . - # /lib/x86_64-linux-gnu/libcrypto.so.1.0.0(+0x625ec) [0x7fe2e1cf15ec] . - # /lib/x86_64-linux-gnu/libcrypto.so.1.0.0(DSA_new_method+0xe6) [0x7fe2e1d524d6] . - # /lib/x86_64-linux-gnu/libcrypto.so.1.0.0(DSA_generate_parameters+0x3a) [0x7fe2e1d5364a] <------ begin interesting - # /home/exarkun/Projects/opentls/trunk/tls/c/__pycache__/_cffi__x305d4698xb539baaa.so(+0x1f397) [0x7fe2df84d397] - # /home/exarkun/Projects/cpython/2.7/python(PyCFunction_Call+0x8b) [0x56265a] - # /home/exarkun/Projects/cpython/2.7/python() [0x4d5f52] - # /home/exarkun/Projects/cpython/2.7/python(PyEval_EvalFrameEx+0x753b) [0x4d0e1e] - # /home/exarkun/Projects/cpython/2.7/python() [0x4d6419] - # ... - # - # Notice the stack is upside down compared to a Python traceback. - # Identify the start and end of interesting bits and stuff it into the stack we report. - - saved = list(c_stack) - - # Figure the first interesting frame will be after a the cffi-compiled module - while c_stack and '/__pycache__/_cffi__' not in c_stack[-1]: - c_stack.pop() - - # Figure the last interesting frame will always be CRYPTO_malloc, - # since that's where we hooked in to things. - while c_stack and 'CRYPTO_malloc' not in c_stack[0] and 'CRYPTO_realloc' not in c_stack[0]: - c_stack.pop(0) - - if c_stack: - c_stack.reverse() - else: - c_stack = saved[::-1] - stack.extend([frame + "\n" for frame in c_stack]) - - stack.insert(0, "Leaked (%s) at:\n") - return "".join(stack) - - if leaks: - unique_leaks = {} - for p in leaks: - size = memdbg.heap[p][-1][0] - new_leak = format_leak(p) - if new_leak not in unique_leaks: - unique_leaks[new_leak] = [(size, p)] - else: - unique_leaks[new_leak].append((size, p)) - memdbg.free(p) - - for (stack, allocs) in unique_leaks.iteritems(): - allocs_accum = [] - for (size, pointer) in allocs: - - addr = int(ffi.cast('uintptr_t', pointer)) - allocs_accum.append("%d@0x%x" % (size, addr)) - allocs_report = ", ".join(sorted(allocs_accum)) - - result.addError( - self, - (None, Exception(stack % (allocs_report,)), None)) - - - def tearDown(self): - """ - Clean up any files or directories created using :py:meth:`TestCase.mktemp`. - Subclasses must invoke this method if they override it or the - cleanup will not occur. - """ - if False and self._temporaryFiles is not None: - for temp in self._temporaryFiles: - if os.path.isdir(temp): - shutil.rmtree(temp) - elif os.path.exists(temp): - os.unlink(temp) - try: - exception_from_error_queue(Error) - except Error: - e = sys.exc_info()[1] - if e.args != ([],): - self.fail("Left over errors in OpenSSL error queue: " + repr(e)) - - - def assertIsInstance(self, instance, classOrTuple, message=None): - """ - Fail if C{instance} is not an instance of the given class or of - one of the given classes. - - @param instance: the object to test the type (first argument of the - C{isinstance} call). - @type instance: any. - @param classOrTuple: the class or classes to test against (second - argument of the C{isinstance} call). - @type classOrTuple: class, type, or tuple. - - @param message: Custom text to include in the exception text if the - assertion fails. - """ - if not isinstance(instance, classOrTuple): - if message is None: - suffix = "" - else: - suffix = ": " + message - self.fail("%r is not an instance of %s%s" % ( - instance, classOrTuple, suffix)) - - - def failUnlessIn(self, containee, container, msg=None): - """ - Fail the test if :py:data:`containee` is not found in :py:data:`container`. - - :param containee: the value that should be in :py:class:`container` - :param container: a sequence type, or in the case of a mapping type, - will follow semantics of 'if key in dict.keys()' - :param msg: if msg is None, then the failure message will be - '%r not in %r' % (first, second) - """ - if containee not in container: - raise self.failureException(msg or "%r not in %r" - % (containee, container)) - return containee - assertIn = failUnlessIn - - def assertNotIn(self, containee, container, msg=None): - """ - Fail the test if C{containee} is found in C{container}. - - @param containee: the value that should not be in C{container} - @param container: a sequence type, or in the case of a mapping type, - will follow semantics of 'if key in dict.keys()' - @param msg: if msg is None, then the failure message will be - '%r in %r' % (first, second) - """ - if containee in container: - raise self.failureException(msg or "%r in %r" - % (containee, container)) - return containee - failIfIn = assertNotIn - - - def failUnlessIdentical(self, first, second, msg=None): - """ - Fail the test if :py:data:`first` is not :py:data:`second`. This is an - obect-identity-equality test, not an object equality - (i.e. :py:func:`__eq__`) test. - - :param msg: if msg is None, then the failure message will be - '%r is not %r' % (first, second) - """ - if first is not second: - raise self.failureException(msg or '%r is not %r' % (first, second)) - return first - assertIdentical = failUnlessIdentical - - - def failIfIdentical(self, first, second, msg=None): - """ - Fail the test if :py:data:`first` is :py:data:`second`. This is an - obect-identity-equality test, not an object equality - (i.e. :py:func:`__eq__`) test. - - :param msg: if msg is None, then the failure message will be - '%r is %r' % (first, second) - """ - if first is second: - raise self.failureException(msg or '%r is %r' % (first, second)) - return first - assertNotIdentical = failIfIdentical - - - def failUnlessRaises(self, exception, f, *args, **kwargs): - """ - Fail the test unless calling the function :py:data:`f` with the given - :py:data:`args` and :py:data:`kwargs` raises :py:data:`exception`. The - failure will report the traceback and call stack of the unexpected - exception. - - :param exception: exception type that is to be expected - :param f: the function to call - - :return: The raised exception instance, if it is of the given type. - :raise self.failureException: Raised if the function call does - not raise an exception or if it raises an exception of a - different type. - """ - try: - result = f(*args, **kwargs) - except exception: - inst = sys.exc_info()[1] - return inst - except: - raise self.failureException('%s raised instead of %s' - % (sys.exc_info()[0], - exception.__name__, - )) - else: - raise self.failureException('%s not raised (%r returned)' - % (exception.__name__, result)) - assertRaises = failUnlessRaises - - - _temporaryFiles = None - def mktemp(self): - """ - Pathetic substitute for twisted.trial.unittest.TestCase.mktemp. - """ - if self._temporaryFiles is None: - self._temporaryFiles = [] - temp = b(mktemp(dir=".")) - self._temporaryFiles.append(temp) - return temp - - - # Other stuff - def assertConsistentType(self, theType, name, *constructionArgs): - """ - Perform various assertions about :py:data:`theType` to ensure that it is a - well-defined type. This is useful for extension types, where it's - pretty easy to do something wacky. If something about the type is - unusual, an exception will be raised. - - :param theType: The type object about which to make assertions. - :param name: A string giving the name of the type. - :param constructionArgs: Positional arguments to use with :py:data:`theType` to - create an instance of it. - """ - self.assertEqual(theType.__name__, name) - self.assertTrue(isinstance(theType, type)) - instance = theType(*constructionArgs) - self.assertIdentical(type(instance), theType) - - - -class EqualityTestsMixin(object): - """ - A mixin defining tests for the standard implementation of C{==} and C{!=}. - """ - def anInstance(self): - """ - Return an instance of the class under test. Each call to this method - must return a different object. All objects returned must be equal to - each other. - """ - raise NotImplementedError() - - - def anotherInstance(self): - """ - Return an instance of the class under test. Each call to this method - must return a different object. The objects must not be equal to the - objects returned by C{anInstance}. They may or may not be equal to - each other (they will not be compared against each other). - """ - raise NotImplementedError() - - - def test_identicalEq(self): - """ - An object compares equal to itself using the C{==} operator. - """ - o = self.anInstance() - self.assertTrue(o == o) - - - def test_identicalNe(self): - """ - An object doesn't compare not equal to itself using the C{!=} operator. - """ - o = self.anInstance() - self.assertFalse(o != o) - - - def test_sameEq(self): - """ - Two objects that are equal to each other compare equal to each other - using the C{==} operator. - """ - a = self.anInstance() - b = self.anInstance() - self.assertTrue(a == b) - - - def test_sameNe(self): - """ - Two objects that are equal to each other do not compare not equal to - each other using the C{!=} operator. - """ - a = self.anInstance() - b = self.anInstance() - self.assertFalse(a != b) - - - def test_differentEq(self): - """ - Two objects that are not equal to each other do not compare equal to - each other using the C{==} operator. - """ - a = self.anInstance() - b = self.anotherInstance() - self.assertFalse(a == b) - - - def test_differentNe(self): - """ - Two objects that are not equal to each other compare not equal to each - other using the C{!=} operator. - """ - a = self.anInstance() - b = self.anotherInstance() - self.assertTrue(a != b) - - - def test_anotherTypeEq(self): - """ - The object does not compare equal to an object of an unrelated type - (which does not implement the comparison) using the C{==} operator. - """ - a = self.anInstance() - b = object() - self.assertFalse(a == b) - - - def test_anotherTypeNe(self): - """ - The object compares not equal to an object of an unrelated type (which - does not implement the comparison) using the C{!=} operator. - """ - a = self.anInstance() - b = object() - self.assertTrue(a != b) - - - def test_delegatedEq(self): - """ - The result of comparison using C{==} is delegated to the right-hand - operand if it is of an unrelated type. - """ - class Delegate(object): - def __eq__(self, other): - # Do something crazy and obvious. - return [self] - - a = self.anInstance() - b = Delegate() - self.assertEqual(a == b, [b]) - - - def test_delegateNe(self): - """ - The result of comparison using C{!=} is delegated to the right-hand - operand if it is of an unrelated type. - """ - class Delegate(object): - def __ne__(self, other): - # Do something crazy and obvious. - return [self] - - a = self.anInstance() - b = Delegate() - self.assertEqual(a != b, [b]) diff --git a/OpenSSL/tsafe.py b/OpenSSL/tsafe.py deleted file mode 100644 index 3a9c71035..000000000 --- a/OpenSSL/tsafe.py +++ /dev/null @@ -1,28 +0,0 @@ -from OpenSSL import SSL -_ssl = SSL -del SSL - -import threading -_RLock = threading.RLock -del threading - -class Connection: - def __init__(self, *args): - self._ssl_conn = _ssl.Connection(*args) - self._lock = _RLock() - - for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', - 'renegotiate', 'bind', 'listen', 'connect', 'accept', - 'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list', - 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', - 'makefile', 'get_app_data', 'set_app_data', 'state_string', - 'sock_shutdown', 'get_peer_certificate', 'get_peer_cert_chain', 'want_read', - 'want_write', 'set_connect_state', 'set_accept_state', - 'connect_ex', 'sendall'): - exec("""def %s(self, *args): - self._lock.acquire() - try: - return self._ssl_conn.%s(*args) - finally: - self._lock.release()\n""" % (f, f)) - diff --git a/OpenSSL/version.py b/OpenSSL/version.py deleted file mode 100644 index 307dba0b1..000000000 --- a/OpenSSL/version.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) AB Strakt -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. - -""" -pyOpenSSL - A simple wrapper around the OpenSSL library -""" - -__version__ = '0.14' diff --git a/README.rst b/README.rst index de9aa4e16..bf98eac11 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,42 @@ +======================================================== +pyOpenSSL -- A Python wrapper around the OpenSSL library +======================================================== -pyOpenSSL - A Python wrapper around the OpenSSL library ------------------------------------------------------------------------------- +.. image:: https://readthedocs.org/projects/pyopenssl/badge/?version=stable + :target: https://pyopenssl.org/en/stable/ + :alt: Stable Docs -See the file INSTALL for installation instructions. +.. image:: https://github.com/pyca/pyopenssl/workflows/CI/badge.svg?branch=main + :target: https://github.com/pyca/pyopenssl/actions?query=workflow%3ACI+branch%3Amain -See http://github.com/pyca/pyopenssl for development. +**Note:** The Python Cryptographic Authority **strongly suggests** the use of `pyca/cryptography`_ +where possible. If you are using pyOpenSSL for anything other than making a TLS connection +**you should move to cryptography and drop your pyOpenSSL dependency**. -See https://mail.python.org/mailman/listinfo/pyopenssl-users for the discussion mailing list. +High-level wrapper around a subset of the OpenSSL library. Includes -.. image:: https://coveralls.io/repos/pyca/pyopenssl/badge.png - :target: https://coveralls.io/r/pyca/pyopenssl +* ``SSL.Connection`` objects, wrapping the methods of Python's portable sockets +* Callbacks written in Python +* Extensive error-handling mechanism, mirroring OpenSSL's error codes + +... and much more. + +You can find more information in the documentation_. +Development takes place on GitHub_. + + +Discussion +========== + +If you run into bugs, you can file them in our `issue tracker`_. + +We maintain a cryptography-dev_ mailing list for both user and development discussions. + +You can also join ``#pyca`` on ``irc.libera.chat`` to ask questions or get involved. + + +.. _documentation: https://pyopenssl.org/ +.. _`issue tracker`: https://github.com/pyca/pyopenssl/issues +.. _cryptography-dev: https://mail.python.org/mailman/listinfo/cryptography-dev +.. _GitHub: https://github.com/pyca/pyopenssl +.. _`pyca/cryptography`: https://github.com/pyca/cryptography diff --git a/TODO b/TODO deleted file mode 100644 index cbcf64244..000000000 --- a/TODO +++ /dev/null @@ -1,8 +0,0 @@ -TODO list - -* Think more carefully about the relation between X509 and X509_NAME - _set_{subject,issuer} dup the new name and free the old one. -* Consider Pyrex -* Updated docs! (rpm, ...) -* _Somehow_ get makefile to work! -* httpslib, imapslib, ftpslib? diff --git a/ChangeLog b/doc/ChangeLog_old.txt similarity index 93% rename from ChangeLog rename to doc/ChangeLog_old.txt index 9ad9317b7..1a16d726b 100644 --- a/ChangeLog +++ b/doc/ChangeLog_old.txt @@ -1,3 +1,61 @@ +This file only contains the changes up to release 0.15.1. Newer changes can be +found at . + +*** + +2015-04-14 Hynek Schlawack + + * Release 0.15.1 + +2015-04-14 Glyph Lefkowitz + + * OpenSSL/SSL.py, OpenSSL/test/test_ssl.py: Fix a regression + present in 0.15, where when an error occurs and no errno() is set, + a KeyError is raised. This happens, for example, if + Connection.shutdown() is called when the underlying transport has + gone away. + +2015-04-14 Hynek Schlawack + + * Release 0.15 + +2015-04-12 Jean-Paul Calderone + + * OpenSSL/rand.py, OpenSSL/SSL.py: APIs which previously accepted + filenames only as bytes now accept them as either bytes or + unicode (and respect sys.getfilesystemencoding()). + +2015-03-23 Jean-Paul Calderone + + * OpenSSL/SSL.py: Add Cory Benfield's next-protocol-negotiation + (NPN) bindings. + +2015-03-15 Jean-Paul Calderone + + * OpenSSL/SSL.py: Add ``Connection.recv_into``, mirroring the + builtin ``socket.recv_into``. Based on work from Cory Benfield. + * OpenSSL/test/test_ssl.py: Add tests for ``recv_into``. + +2015-01-30 Stephen Holsapple + + * OpenSSL/crypto.py: Expose ``X509StoreContext`` for verifying certificates. + * OpenSSL/test/test_crypto.py: Add intermediate certificates for + +2015-01-08 Paul Aurich + + * OpenSSL/SSL.py: ``Connection.shutdown`` now propagates errors from the + underlying socket. + +2014-12-11 Jean-Paul Calderone + + * OpenSSL/SSL.py: Fixed a regression ``Context.check_privatekey`` + causing it to always succeed - even if it should fail. + +2014-08-21 Alex Gaynor + + * OpenSSL/crypto.py: Fixed a regression where calling ``load_pkcs7_data`` + with ``FILETYPE_ASN1`` would fail with a ``NameError``. + 2014-05-05 Jean-Paul Calderone * OpenSSL/SSL.py: Fix a regression in which the first argument of @@ -55,6 +113,10 @@ * OpenSSL/crypto.py: Add ``get_extensions`` method to ``X509Req``. +2014-02-23 Jean-Paul Calderone + + * Release 0.14 + 2014-01-09 Jean-Paul Calderone * OpenSSL: Port to the cffi-based OpenSSL bindings provided by @@ -678,7 +740,7 @@ 2002-06-13 Martin Sjögren * src/ssl/context.c: Changed global_verify_callback so that it uses - PyObject_IsTrue instead of requring ints. + PyObject_IsTrue instead of requiring ints. * Added pymemcompat.h to make the memory management uniform and backwards-compatible. * src/util.h: Added conditional definition of PyModule_AddObject and diff --git a/doc/README b/doc/README index 2a525bb27..2c6359c1e 100644 --- a/doc/README +++ b/doc/README @@ -1,4 +1,4 @@ This is the pyOpenSSL documentation source. It uses Sphinx. To build the -documentation, install Sphinx 1.0 and run: +documentation, install Sphinx and run: $ make html diff --git a/doc/api.rst b/doc/api.rst index 826ec4da2..b5ca3f2d4 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -14,5 +14,4 @@ OpenSSL library. The following modules are defined: :maxdepth: 2 api/crypto - api/rand api/ssl diff --git a/doc/api/crypto.rst b/doc/api/crypto.rst index b360e89c0..9f8b2de94 100644 --- a/doc/api/crypto.rst +++ b/doc/api/crypto.rst @@ -6,228 +6,55 @@ .. py:module:: OpenSSL.crypto :synopsis: Generic cryptographic module +.. danger:: -.. py:data:: X509Type + **This module is pending deprecation, use pyca/cryptography instead.** - See :py:class:`X509`. + `pyca/cryptography`_ is likely a better choice than using this module. + It contains a complete set of cryptographic primitives as well as a significantly better and more powerful X509 API. + If necessary you can convert to and from cryptography objects using the ``to_cryptography`` and ``from_cryptography`` methods on ``X509``, ``CRL``, and ``PKey``. -.. py:class:: X509() - - A class representing X.509 certificates. - - -.. py:data:: X509NameType - - See :py:class:`X509Name`. - - -.. py:class:: X509Name(x509name) - - A class representing X.509 Distinguished Names. - - This constructor creates a copy of *x509name* which should be an - instance of :py:class:`X509Name`. - - -.. py:data:: X509ReqType - - See :py:class:`X509Req`. - - -.. py:class:: X509Req() - - A class representing X.509 certificate requests. - - -.. py:data:: X509StoreType - - A Python type object representing the X509Store object type. - - -.. py:data:: PKeyType - - See :py:class:`PKey`. - - -.. py:class:: PKey() - - A class representing DSA or RSA keys. - - -.. py:data:: PKCS7Type - - A Python type object representing the PKCS7 object type. - - -.. py:data:: PKCS12Type - - A Python type object representing the PKCS12 object type. - - -.. py:data:: X509ExtensionType - - See :py:class:`X509Extension`. - - -.. py:class:: X509Extension(typename, critical, value[, subject][, issuer]) - - A class representing an X.509 v3 certificate extensions. See - http://openssl.org/docs/apps/x509v3_config.html#STANDARD_EXTENSIONS for - *typename* strings and their options. Optional parameters *subject* and - *issuer* must be X509 objects. - - -.. py:data:: NetscapeSPKIType - - See :py:class:`NetscapeSPKI`. - - -.. py:class:: NetscapeSPKI([enc]) - - A class representing Netscape SPKI objects. - - If the *enc* argument is present, it should be a base64-encoded string - representing a NetscapeSPKI object, as returned by the :py:meth:`b64_encode` - method. - - -.. py:class:: CRL() - - A class representing Certifcate Revocation List objects. +Elliptic curves +--------------- +.. autofunction:: get_elliptic_curves -.. py:class:: Revoked() +.. autofunction:: get_elliptic_curve - A class representing Revocation objects of CRL. +Serialization and deserialization +--------------------------------- +The following serialization functions take one of these constants to determine the format. .. py:data:: FILETYPE_PEM - FILETYPE_ASN1 - - File type constants. - - -.. py:data:: TYPE_RSA - TYPE_DSA - - Key type constants. - - -.. py:exception:: Error - - Generic exception used in the :py:mod:`.crypto` module. - - -.. py:function:: get_elliptic_curves - - Return a set of objects representing the elliptic curves supported in the - OpenSSL build in use. - - The curve objects have a :py:class:`unicode` ``name`` attribute by which - they identify themselves. - - The curve objects are useful as values for the argument accepted by - :py:meth:`Context.set_tmp_ecdh` to specify which elliptical curve should be - used for ECDHE key exchange. - - -.. py:function:: get_elliptic_curve - - Return a single curve object selected by name. - - See :py:func:`get_elliptic_curves` for information about curve objects. - - If the named curve is not supported then :py:class:`ValueError` is raised. - - -.. py:function:: dump_certificate(type, cert) - - Dump the certificate *cert* into a buffer string encoded with the type - *type*. - - -.. py:function:: dump_certificate_request(type, req) - - Dump the certificate request *req* into a buffer string encoded with the - type *type*. - - -.. py:function:: dump_privatekey(type, pkey[, cipher, passphrase]) - - Dump the private key *pkey* into a buffer string encoded with the type - *type*, optionally (if *type* is :py:const:`FILETYPE_PEM`) encrypting it - using *cipher* and *passphrase*. - - *passphrase* must be either a string or a callback for providing the - pass phrase. - - -.. py:function:: load_certificate(type, buffer) - - Load a certificate (X509) from the string *buffer* encoded with the - type *type*. - - -.. py:function:: load_certificate_request(type, buffer) - - Load a certificate request (X509Req) from the string *buffer* encoded with - the type *type*. - - -.. py:function:: load_privatekey(type, buffer[, passphrase]) - - Load a private key (PKey) from the string *buffer* encoded with the type - *type* (must be one of :py:const:`FILETYPE_PEM` and - :py:const:`FILETYPE_ASN1`). - - *passphrase* must be either a string or a callback for providing the pass - phrase. - - -.. py:function:: load_crl(type, buffer) - - Load Certificate Revocation List (CRL) data from a string *buffer*. - *buffer* encoded with the type *type*. The type *type* must either - :py:const:`FILETYPE_PEM` or :py:const:`FILETYPE_ASN1`). - -.. py:function:: load_pkcs7_data(type, buffer) +:data:`FILETYPE_PEM` serializes data to a Base64-encoded encoded representation of the underlying ASN.1 data structure. This representation includes delimiters that define what data structure is contained within the Base64-encoded block: for example, for a certificate, the delimiters are ``-----BEGIN CERTIFICATE-----`` and ``-----END CERTIFICATE-----``. - Load pkcs7 data from the string *buffer* encoded with the type *type*. +.. py:data:: FILETYPE_ASN1 +:data:`FILETYPE_ASN1` serializes data to the underlying ASN.1 data structure. The format used by :data:`FILETYPE_ASN1` is also sometimes referred to as DER. -.. py:function:: load_pkcs12(buffer[, passphrase]) +Certificates +~~~~~~~~~~~~ - Load pkcs12 data from the string *buffer*. If the pkcs12 structure is - encrypted, a *passphrase* must be included. The MAC is always - checked and thus required. +.. autofunction:: dump_certificate - See also the man page for the C function :py:func:`PKCS12_parse`. +.. autofunction:: load_certificate +Private keys +~~~~~~~~~~~~ -.. py:function:: sign(key, data, digest) +.. autofunction:: dump_privatekey - Sign a data string using the given key and message digest. +.. autofunction:: load_privatekey - *key* is a :py:class:`PKey` instance. *data* is a ``str`` instance. - *digest* is a ``str`` naming a supported message digest type, for example - :py:const:`sha1`. +Public keys +~~~~~~~~~~~ - .. versionadded:: 0.11 +.. autofunction:: dump_publickey - -.. py:function:: verify(certificate, signature, data, digest) - - Verify the signature for a data string. - - *certificate* is a :py:class:`X509` instance corresponding to the private - key which generated the signature. *signature* is a *str* instance giving - the signature itself. *data* is a *str* instance giving the data to which - the signature applies. *digest* is a *str* instance naming the message - digest type of the signature, for example :py:const:`sha1`. - - .. versionadded:: 0.11 +.. autofunction:: load_publickey .. _openssl-x509: @@ -235,548 +62,92 @@ X509 objects ------------ -X509 objects have the following methods: - -.. py:method:: X509.get_issuer() - - Return an X509Name object representing the issuer of the certificate. - - -.. py:method:: X509.get_pubkey() - - Return a :py:class:`PKey` object representing the public key of the certificate. - - -.. py:method:: X509.get_serial_number() - - Return the certificate serial number. - - -.. py:method:: X509.get_signature_algorithm() - - Return the signature algorithm used in the certificate. If the algorithm is - undefined, raise :py:data:`ValueError`. - - ..versionadded:: 0.13 - - -.. py:method:: X509.get_subject() - - Return an :py:class:`X509Name` object representing the subject of the certificate. - - -.. py:method:: X509.get_version() - - Return the certificate version. - - -.. py:method:: X509.get_notBefore() - - Return a string giving the time before which the certificate is not valid. The - string is formatted as an ASN1 GENERALIZEDTIME:: - - YYYYMMDDhhmmssZ - YYYYMMDDhhmmss+hhmm - YYYYMMDDhhmmss-hhmm - - If no value exists for this field, :py:data:`None` is returned. - - -.. py:method:: X509.get_notAfter() - - Return a string giving the time after which the certificate is not valid. The - string is formatted as an ASN1 GENERALIZEDTIME:: - - YYYYMMDDhhmmssZ - YYYYMMDDhhmmss+hhmm - YYYYMMDDhhmmss-hhmm - - If no value exists for this field, :py:data:`None` is returned. - - -.. py:method:: X509.set_notBefore(when) - - Change the time before which the certificate is not valid. *when* is a - string formatted as an ASN1 GENERALIZEDTIME:: - - YYYYMMDDhhmmssZ - YYYYMMDDhhmmss+hhmm - YYYYMMDDhhmmss-hhmm - - -.. py:method:: X509.set_notAfter(when) - - Change the time after which the certificate is not valid. *when* is a - string formatted as an ASN1 GENERALIZEDTIME:: - - YYYYMMDDhhmmssZ - YYYYMMDDhhmmss+hhmm - YYYYMMDDhhmmss-hhmm - - - -.. py:method:: X509.gmtime_adj_notBefore(time) - - Adjust the timestamp (in GMT) when the certificate starts being valid. - - -.. py:method:: X509.gmtime_adj_notAfter(time) - - Adjust the timestamp (in GMT) when the certificate stops being valid. - - -.. py:method:: X509.has_expired() - - Checks the certificate's time stamp against current time. Returns true if the - certificate has expired and false otherwise. - - -.. py:method:: X509.set_issuer(issuer) - - Set the issuer of the certificate to *issuer*. - - -.. py:method:: X509.set_pubkey(pkey) - - Set the public key of the certificate to *pkey*. - - -.. py:method:: X509.set_serial_number(serialno) - - Set the serial number of the certificate to *serialno*. - - -.. py:method:: X509.set_subject(subject) - - Set the subject of the certificate to *subject*. - - -.. py:method:: X509.set_version(version) - - Set the certificate version to *version*. - - -.. py:method:: X509.sign(pkey, digest) - - Sign the certificate, using the key *pkey* and the message digest algorithm - identified by the string *digest*. - - -.. py:method:: X509.subject_name_hash() - - Return the hash of the certificate subject. - -.. py:method:: X509.digest(digest_name) - - Return a digest of the certificate, using the *digest_name* method. - *digest_name* must be a string describing a digest algorithm supported - by OpenSSL (by EVP_get_digestbyname, specifically). For example, - :py:const:`"md5"` or :py:const:`"sha1"`. - - -.. py:method:: X509.add_extensions(extensions) - - Add the extensions in the sequence *extensions* to the certificate. - - -.. py:method:: X509.get_extension_count() - - Return the number of extensions on this certificate. - - .. versionadded:: 0.12 - - -.. py:method:: X509.get_extension(index) - - Retrieve the extension on this certificate at the given index. - - Extensions on a certificate are kept in order. The index parameter selects - which extension will be returned. The returned object will be an - :py:class:`X509Extension` instance. - - .. versionadded:: 0.12 - +.. autoclass:: X509 + :members: .. _openssl-x509name: X509Name objects ---------------- -X509Name objects have the following methods: - -.. py:method:: X509Name.hash() - - Return an integer giving the first four bytes of the MD5 digest of the DER - representation of the name. - - -.. py:method:: X509Name.der() - - Return a string giving the DER representation of the name. - - -.. py:method:: X509Name.get_components() - - Return a list of two-tuples of strings giving the components of the name. - - -X509Name objects have the following members: - -.. py:attribute:: X509Name.countryName - - The country of the entity. :py:attr:`C` may be used as an alias for - :py:attr:`countryName`. - - -.. py:attribute:: X509Name.stateOrProvinceName - - The state or province of the entity. :py:attr:`ST` may be used as an alias for - :py:attr:`stateOrProvinceName`. - - -.. py:attribute:: X509Name.localityName - - The locality of the entity. :py:attr:`L` may be used as an alias for - :py:attr:`localityName`. - - -.. py:attribute:: X509Name.organizationName - - The organization name of the entity. :py:attr:`O` may be used as an alias for - :py:attr:`organizationName`. - - -.. py:attribute:: X509Name.organizationalUnitName - - The organizational unit of the entity. :py:attr:`OU` may be used as an alias for - :py:attr:`organizationalUnitName`. - - -.. py:attribute:: X509Name.commonName - - The common name of the entity. :py:attr:`CN` may be used as an alias for - :py:attr:`commonName`. - - -.. py:attribute:: X509Name.emailAddress - - The e-mail address of the entity. - - -.. _openssl-x509req: - -X509Req objects ---------------- - -X509Req objects have the following methods: - -.. py:method:: X509Req.get_pubkey() - - Return a :py:class:`PKey` object representing the public key of the certificate request. - - -.. py:method:: X509Req.get_subject() - - Return an :py:class:`X509Name` object representing the subject of the certificate. - - -.. py:method:: X509Req.set_pubkey(pkey) - - Set the public key of the certificate request to *pkey*. - - -.. py:method:: X509Req.sign(pkey, digest) - - Sign the certificate request, using the key *pkey* and the message digest - algorithm identified by the string *digest*. - - -.. py:method:: X509Req.verify(pkey) - - Verify a certificate request using the public key *pkey*. - - -.. py:method:: X509Req.set_version(version) - - Set the version (RFC 2459, 4.1.2.1) of the certificate request to - *version*. - - -.. py:method:: X509Req.get_version() - - Get the version (RFC 2459, 4.1.2.1) of the certificate request. - - -.. py:method:: X509Req.get_extensions() - - Get extensions to the request. - - .. versionadded:: 0.15 - +.. autoclass:: X509Name + :members: + :special-members: + :exclude-members: __repr__, __getattr__, __weakref__ .. _openssl-x509store: X509Store objects ----------------- -The X509Store object has currently just one method: - -.. py:method:: X509Store.add_cert(cert) - - Add the certificate *cert* to the certificate store. - - -.. _openssl-pkey: - -PKey objects ------------- - -The PKey object has the following methods: - -.. py:method:: PKey.bits() - - Return the number of bits of the key. - - -.. py:method:: PKey.generate_key(type, bits) - - Generate a public/private key pair of the type *type* (one of - :py:const:`TYPE_RSA` and :py:const:`TYPE_DSA`) with the size *bits*. - - -.. py:method:: PKey.type() - - Return the type of the key. - - -.. py:method:: PKey.check() - - Check the consistency of this key, returning True if it is consistent and - raising an exception otherwise. This is only valid for RSA keys. See the - OpenSSL RSA_check_key man page for further limitations. - - -.. _openssl-pkcs7: - -PKCS7 objects -------------- - -PKCS7 objects have the following methods: - -.. py:method:: PKCS7.type_is_signed() - - FIXME - - -.. py:method:: PKCS7.type_is_enveloped() - - FIXME - - -.. py:method:: PKCS7.type_is_signedAndEnveloped() - - FIXME - - -.. py:method:: PKCS7.type_is_data() - - FIXME - - -.. py:method:: PKCS7.get_type_name() +.. autoclass:: X509Store + :members: - Get the type name of the PKCS7. +.. _openssl-x509storecontexterror: +X509StoreContextError objects +----------------------------- -.. _openssl-pkcs12: +.. autoclass:: X509StoreContextError + :members: -PKCS12 objects --------------- +.. _openssl-x509storecontext: -PKCS12 objects have the following methods: +X509StoreContext objects +------------------------ -.. py:method:: PKCS12.export([passphrase=None][, iter=2048][, maciter=1]) +.. autoclass:: X509StoreContext + :members: - Returns a PKCS12 object as a string. - - The optional *passphrase* must be a string not a callback. - - See also the man page for the C function :py:func:`PKCS12_create`. - - -.. py:method:: PKCS12.get_ca_certificates() - - Return CA certificates within the PKCS12 object as a tuple. Returns - :py:const:`None` if no CA certificates are present. - - -.. py:method:: PKCS12.get_certificate() - - Return certificate portion of the PKCS12 structure. - - -.. py:method:: PKCS12.get_friendlyname() - - Return friendlyName portion of the PKCS12 structure. - - -.. py:method:: PKCS12.get_privatekey() - - Return private key portion of the PKCS12 structure - - -.. py:method:: PKCS12.set_ca_certificates(cacerts) - - Replace or set the CA certificates within the PKCS12 object with the sequence *cacerts*. - - Set *cacerts* to :py:const:`None` to remove all CA certificates. - - -.. py:method:: PKCS12.set_certificate(cert) - - Replace or set the certificate portion of the PKCS12 structure. - - -.. py:method:: PKCS12.set_friendlyname(name) - - Replace or set the friendlyName portion of the PKCS12 structure. - - -.. py:method:: PKCS12.set_privatekey(pkey) - - Replace or set private key portion of the PKCS12 structure - - -.. _openssl-509ext: - -X509Extension objects ---------------------- - -X509Extension objects have several methods: - -.. py:method:: X509Extension.get_critical() - - Return the critical field of the extension object. - - -.. py:method:: X509Extension.get_short_name() - - Retrieve the short descriptive name for this extension. - - The result is a byte string like :py:const:`basicConstraints`. - - .. versionadded:: 0.12 - - -.. py:method:: X509Extension.get_data() - - Retrieve the data for this extension. - - The result is the ASN.1 encoded form of the extension data as a byte string. - - .. versionadded:: 0.12 - - -.. _openssl-netscape-spki: - -NetscapeSPKI objects --------------------- - -NetscapeSPKI objects have the following methods: - -.. py:method:: NetscapeSPKI.b64_encode() - - Return a base64-encoded string representation of the object. - - -.. py:method:: NetscapeSPKI.get_pubkey() - - Return the public key of object. - - -.. py:method:: NetscapeSPKI.set_pubkey(key) - - Set the public key of the object to *key*. - - -.. py:method:: NetscapeSPKI.sign(key, digest_name) - - Sign the NetscapeSPKI object using the given *key* and *digest_name*. - *digest_name* must be a string describing a digest algorithm supported by - OpenSSL (by EVP_get_digestbyname, specifically). For example, - :py:const:`"md5"` or :py:const:`"sha1"`. - - -.. py:method:: NetscapeSPKI.verify(key) - - Verify the NetscapeSPKI object using the given *key*. - - -.. _crl: - -CRL objects ------------ - -CRL objects have the following methods: - -.. py:method:: CRL.add_revoked(revoked) - - Add a Revoked object to the CRL, by value not reference. - - -.. py:method:: CRL.export(cert, key[, type=FILETYPE_PEM][, days=100]) - - Use *cert* and *key* to sign the CRL and return the CRL as a string. - *days* is the number of days before the next CRL is due. - - -.. py:method:: CRL.get_revoked() - - Return a tuple of Revoked objects, by value not reference. - - -.. _revoked: - -Revoked objects ---------------- - -Revoked objects have the following methods: - -.. py:method:: Revoked.all_reasons() - - Return a list of all supported reasons. - - -.. py:method:: Revoked.get_reason() +.. _openssl-pkey: - Return the revocation reason as a str. Can be - None, which differs from "Unspecified". +X509StoreFlags constants +------------------------ +.. autoclass:: X509StoreFlags -.. py:method:: Revoked.get_rev_date() + .. data:: CRL_CHECK + .. data:: CRL_CHECK_ALL + .. data:: IGNORE_CRITICAL + .. data:: X509_STRICT + .. data:: ALLOW_PROXY_CERTS + .. data:: POLICY_CHECK + .. data:: EXPLICIT_POLICY + .. data:: INHIBIT_MAP + .. data:: NOTIFY_POLICY + .. data:: CHECK_SS_SIGNATURE + .. data:: PARTIAL_CHAIN - Return the revocation date as a str. - The string is formatted as an ASN1 GENERALIZEDTIME. +.. _openssl-x509storeflags: +PKey objects +------------ -.. py:method:: Revoked.get_serial() +.. autoclass:: PKey + :members: - Return a str containing a hex number of the serial of the revoked certificate. +.. py:data:: TYPE_RSA + TYPE_DSA + Key type constants. -.. py:method:: Revoked.set_reason(reason) +Exceptions +---------- - Set the revocation reason. *reason* must be None or a string, but the - values are limited. Spaces and case are ignored. See - :py:meth:`all_reasons`. +.. py:exception:: Error + Generic exception used in the :py:mod:`.crypto` module. -.. py:method:: Revoked.set_rev_date(date) - Set the revocation date. - The string is formatted as an ASN1 GENERALIZEDTIME. +Digest names +------------ +Several of the functions and methods in this module take a digest name. +These must be strings describing a digest algorithm supported by OpenSSL (by ``EVP_get_digestbyname``, specifically). +For example, :const:`b"sha256"` or :const:`b"sha384"`. -.. py:method:: Revoked.set_serial(serial) +More information and a list of these digest names can be found in the ``EVP_DigestInit(3)`` man page of your OpenSSL installation. +This page can be found online for the latest version of OpenSSL: +https://www.openssl.org/docs/manmaster/man3/EVP_DigestInit.html - *serial* is a string containing a hex number of the serial of the revoked certificate. +.. _`pyca/cryptography`: https://cryptography.io diff --git a/doc/api/rand.rst b/doc/api/rand.rst deleted file mode 100644 index 18789b8e1..000000000 --- a/doc/api/rand.rst +++ /dev/null @@ -1,79 +0,0 @@ -.. _openssl-rand: - -:py:mod:`rand` --- An interface to the OpenSSL pseudo random number generator -============================================================================= - -.. py:module:: OpenSSL.rand - :synopsis: An interface to the OpenSSL pseudo random number generator - - -This module handles the OpenSSL pseudo random number generator (PRNG) and -declares the following: - -.. py:function:: add(string, entropy) - - Mix bytes from *string* into the PRNG state. The *entropy* argument is - (the lower bound of) an estimate of how much randomness is contained in - *string*, measured in bytes. For more information, see e.g. :rfc:`1750`. - - -.. py:function:: bytes(num_bytes) - - Get some random bytes from the PRNG as a string. - - This is a wrapper for the C function :py:func:`RAND_bytes`. - - -.. py:function:: cleanup() - - Erase the memory used by the PRNG. - - This is a wrapper for the C function :py:func:`RAND_cleanup`. - - -.. py:function:: egd(path[, bytes]) - - Query the `Entropy Gathering Daemon `_ on - socket *path* for *bytes* bytes of random data and uses :py:func:`add` to - seed the PRNG. The default value of *bytes* is 255. - - -.. py:function:: load_file(path[, bytes]) - - Read *bytes* bytes (or all of it, if *bytes* is negative) of data from the - file *path* to seed the PRNG. The default value of *bytes* is -1. - - -.. py:function:: screen() - - Add the current contents of the screen to the PRNG state. - - Availability: Windows. - - -.. py:function:: seed(string) - - This is equivalent to calling :py:func:`add` with *entropy* as the length - of the string. - - -.. py:function:: status() - - Returns true if the PRNG has been seeded with enough data, and false otherwise. - - -.. py:function:: write_file(path) - - Write a number of random bytes (currently 1024) to the file *path*. This - file can then be used with :py:func:`load_file` to seed the PRNG again. - - -.. py:exception:: Error - - If the current RAND method supports any errors, this is raised when needed. - The default method does not raise this when the entropy pool is depleted. - - Whenever this exception is raised directly, it has a list of error messages - from the OpenSSL error queue, where each item is a tuple *(lib, function, - reason)*. Here *lib*, *function* and *reason* are all strings, describing - where and what the problem is. See :manpage:`err(3)` for more information. diff --git a/doc/api/ssl.rst b/doc/api/ssl.rst index a75af1f7d..ea7416d53 100644 --- a/doc/api/ssl.rst +++ b/doc/api/ssl.rst @@ -10,7 +10,10 @@ This module handles things specific to SSL. There are two objects defined: Context, Connection. -.. py:data:: SSLv2_METHOD +.. py:data:: TLS_METHOD + TLS_SERVER_METHOD + TLS_CLIENT_METHOD + SSLv2_METHOD SSLv3_METHOD SSLv23_METHOD TLSv1_METHOD @@ -18,11 +21,21 @@ Context, Connection. TLSv1_2_METHOD These constants represent the different SSL methods to use when creating a - context object. If the underlying OpenSSL build is missing support for any - of these protocols, constructing a :py:class:`Context` using the + context object. New code should only use ``TLS_METHOD``, ``TLS_SERVER_METHOD``, + or ``TLS_CLIENT_METHOD``. If the underlying OpenSSL build is missing support + for any of these protocols, constructing a :py:class:`Context` using the corresponding :py:const:`*_METHOD` will raise an exception. +.. py:data:: SSL3_VERSION + TLS1_VERSION + TLS1_1_VERSION + TLS1_2_VERSION + TLS1_3_VERSION + + These constants represent the different TLS versions to use when + setting the minimum or maximum TLS version. + .. py:data:: VERIFY_NONE VERIFY_PEER VERIFY_FAIL_IF_NO_PEER_CERT @@ -39,11 +52,12 @@ Context, Connection. .. py:data:: OP_SINGLE_DH_USE + OP_SINGLE_ECDH_USE - Constant used with :py:meth:`set_options` of Context objects. + Constants used with :py:meth:`set_options` of Context objects. - When this option is used, a new key will always be created when using - ephemeral Diffie-Hellman. + When these options are used, a new key will always be created when using + ephemeral (Elliptic curve) Diffie-Hellman. .. py:data:: OP_EPHEMERAL_RSA @@ -73,6 +87,7 @@ Context, Connection. OP_NO_TLSv1 OP_NO_TLSv1_1 OP_NO_TLSv1_2 + OP_NO_TLSv1_3 Constants used with :py:meth:`set_options` of Context objects. @@ -83,14 +98,20 @@ Context, Connection. :py:const:`OP_NO_*` constant may be undefined. -.. py:data:: SSLEAY_VERSION - SSLEAY_CFLAGS - SSLEAY_BUILT_ON - SSLEAY_PLATFORM - SSLEAY_DIR +.. py:data:: OPENSSL_VERSION + OPENSSL_CFLAGS + OPENSSL_BUILT_ON + OPENSSL_PLATFORM + OPENSSL_DIR + + .. versionchanged:: 22.1.0 - Constants used with :py:meth:`SSLeay_version` to specify what OpenSSL version - information to retrieve. See the man page for the :py:func:`SSLeay_version` C + Previously these were all named ``SSLEAY_*``. Those names are still + available for backwards compatibility, but the ``OPENSSL_*`` names are + preferred. + + Constants used with :py:meth:`OpenSSL_version` to specify what OpenSSL version + information to retrieve. See the man page for the :py:func:`OpenSSL_version` C API for details. @@ -117,43 +138,26 @@ Context, Connection. for details. -.. py:function:: SSLeay_version(type) - - Retrieve a string describing some aspect of the underlying OpenSSL version. The - type passed in should be one of the :py:const:`SSLEAY_*` constants defined in - this module. - - -.. py:data:: ContextType +.. py:data:: NO_OVERLAPPING_PROTOCOLS - See :py:class:`Context`. + A sentinel value that can be returned by the callback passed to + :py:meth:`Context.set_alpn_select_callback` to indicate that + the handshake can continue without a specific application protocol. + .. versionadded:: 19.1 -.. py:class:: Context(method) - A class representing SSL contexts. Contexts define the parameters of one or - more SSL connections. +.. autofunction:: OpenSSL_version - *method* should be :py:const:`SSLv2_METHOD`, :py:const:`SSLv3_METHOD`, - :py:const:`SSLv23_METHOD`, :py:const:`TLSv1_METHOD`, :py:const:`TLSv1_1_METHOD`, - or :py:const:`TLSv1_2_METHOD`. +.. autoclass:: Context + :noindex: -.. py:class:: Session() - - A class representing an SSL session. A session defines certain connection - parameters which may be re-used to speed up the setup of subsequent - connections. - - .. versionadded:: 0.14 - - -.. py:data:: ConnectionType - - See :py:class:`Connection`. +.. autoclass:: Session .. py:class:: Connection(context, socket) + :noindex: A class representing SSL connections. @@ -235,242 +239,8 @@ Context objects Context objects have the following methods: -.. :py:class:: OpenSSL.SSL.Context - -.. py:method:: Context.check_privatekey() - - Check if the private key (loaded with :py:meth:`use_privatekey`) matches the - certificate (loaded with :py:meth:`use_certificate`). Returns - :py:data:`None` if they match, raises :py:exc:`Error` otherwise. - - -.. py:method:: Context.get_app_data() - - Retrieve application data as set by :py:meth:`set_app_data`. - - -.. py:method:: Context.get_cert_store() - - Retrieve the certificate store (a X509Store object) that the context uses. - This can be used to add "trusted" certificates without using the - :py:meth:`load_verify_locations` method. - - -.. py:method:: Context.get_timeout() - - Retrieve session timeout, as set by :py:meth:`set_timeout`. The default is 300 - seconds. - - -.. py:method:: Context.get_verify_depth() - - Retrieve the Context object's verify depth, as set by - :py:meth:`set_verify_depth`. - - -.. py:method:: Context.get_verify_mode() - - Retrieve the Context object's verify mode, as set by :py:meth:`set_verify`. - - -.. py:method:: Context.load_client_ca(pemfile) - - Read a file with PEM-formatted certificates that will be sent to the client - when requesting a client certificate. - - -.. py:method:: Context.set_client_ca_list(certificate_authorities) - - Replace the current list of preferred certificate signers that would be - sent to the client when requesting a client certificate with the - *certificate_authorities* sequence of :py:class:`OpenSSL.crypto.X509Name`'s. - - .. versionadded:: 0.10 - - -.. py:method:: Context.add_client_ca(certificate_authority) - - Extract a :py:class:`OpenSSL.crypto.X509Name` from the *certificate_authority* - :py:class:`OpenSSL.crypto.X509` certificate and add it to the list of preferred - certificate signers sent to the client when requesting a client certificate. - - .. versionadded:: 0.10 - - -.. py:method:: Context.load_verify_locations(pemfile, capath) - - Specify where CA certificates for verification purposes are located. These - are trusted certificates. Note that the certificates have to be in PEM - format. If capath is passed, it must be a directory prepared using the - ``c_rehash`` tool included with OpenSSL. Either, but not both, of - *pemfile* or *capath* may be :py:data:`None`. - - -.. py:method:: Context.set_default_verify_paths() - - Specify that the platform provided CA certificates are to be used for - verification purposes. This method may not work properly on OS X. - - -.. py:method:: Context.load_tmp_dh(dhfile) - - Load parameters for Ephemeral Diffie-Hellman from *dhfile*. - - -.. py:method:: Context.set_tmp_ecdh(curve) - - Select a curve to use for ECDHE key exchange. - - The valid values of *curve* are the objects returned by - :py:func:`OpenSSL.crypto.get_elliptic_curves` or - :py:func:`OpenSSL.crypto.get_elliptic_curve`. - - -.. py:method:: Context.set_app_data(data) - - Associate *data* with this Context object. *data* can be retrieved - later using the :py:meth:`get_app_data` method. - - -.. py:method:: Context.set_cipher_list(ciphers) - - Set the list of ciphers to be used in this context. See the OpenSSL manual for - more information (e.g. :manpage:`ciphers(1)`) - - -.. py:method:: Context.set_info_callback(callback) - - Set the information callback to *callback*. This function will be called - from time to time during SSL handshakes. - - *callback* should take three arguments: a Connection object and two integers. - The first integer specifies where in the SSL handshake the function was - called, and the other the return code from a (possibly failed) internal - function call. - - -.. py:method:: Context.set_options(options) - - Add SSL options. Options you have set before are not cleared! - This method should be used with the :py:const:`OP_*` constants. - - -.. py:method:: Context.set_mode(mode) - - Add SSL mode. Modes you have set before are not cleared! This method should - be used with the :py:const:`MODE_*` constants. - - -.. py:method:: Context.set_passwd_cb(callback[, userdata]) - - Set the passphrase callback to *callback*. This function will be called - when a private key with a passphrase is loaded. *callback* must accept - three positional arguments. First, an integer giving the maximum length of - the passphrase it may return. If the returned passphrase is longer than - this, it will be truncated. Second, a boolean value which will be true if - the user should be prompted for the passphrase twice and the callback should - verify that the two values supplied are equal. Third, the value given as the - *userdata* parameter to :py:meth:`set_passwd_cb`. If an error occurs, - *callback* should return a false value (e.g. an empty string). - - -.. py:method:: Context.set_session_cache_mode(mode) - - Set the behavior of the session cache used by all connections using this - Context. The previously set mode is returned. See :py:const:`SESS_CACHE_*` - for details about particular modes. - - .. versionadded:: 0.14 - - -.. py:method:: Context.get_session_cache_mode() - - Get the current session cache mode. - - .. versionadded:: 0.14 - - -.. py:method:: Context.set_session_id(name) - - Set the context *name* within which a session can be reused for this - Context object. This is needed when doing session resumption, because there is - no way for a stored session to know which Context object it is associated with. - *name* may be any binary data. - - -.. py:method:: Context.set_timeout(timeout) - - Set the timeout for newly created sessions for this Context object to - *timeout*. *timeout* must be given in (whole) seconds. The default - value is 300 seconds. See the OpenSSL manual for more information (e.g. - :manpage:`SSL_CTX_set_timeout(3)`). - - -.. py:method:: Context.set_verify(mode, callback) - - Set the verification flags for this Context object to *mode* and specify - that *callback* should be used for verification callbacks. *mode* should be - one of :py:const:`VERIFY_NONE` and :py:const:`VERIFY_PEER`. If - :py:const:`VERIFY_PEER` is used, *mode* can be OR:ed with - :py:const:`VERIFY_FAIL_IF_NO_PEER_CERT` and :py:const:`VERIFY_CLIENT_ONCE` - to further control the behaviour. - - *callback* should take five arguments: A Connection object, an X509 object, - and three integer variables, which are in turn potential error number, error - depth and return code. *callback* should return true if verification passes - and false otherwise. - - -.. py:method:: Context.set_verify_depth(depth) - - Set the maximum depth for the certificate chain verification that shall be - allowed for this Context object. - - -.. py:method:: Context.use_certificate(cert) - - Use the certificate *cert* which has to be a X509 object. - - -.. py:method:: Context.add_extra_chain_cert(cert) - - Adds the certificate *cert*, which has to be a X509 object, to the - certificate chain presented together with the certificate. - - -.. py:method:: Context.use_certificate_chain_file(file) - - Load a certificate chain from *file* which must be PEM encoded. - - -.. py:method:: Context.use_privatekey(pkey) - - Use the private key *pkey* which has to be a PKey object. - - -.. py:method:: Context.use_certificate_file(file[, format]) - - Load the first certificate found in *file*. The certificate must be in the - format specified by *format*, which is either :py:const:`FILETYPE_PEM` or - :py:const:`FILETYPE_ASN1`. The default is :py:const:`FILETYPE_PEM`. - - -.. py:method:: Context.use_privatekey_file(file[, format]) - - Load the first private key found in *file*. The private key must be in the - format specified by *format*, which is either :py:const:`FILETYPE_PEM` or - :py:const:`FILETYPE_ASN1`. The default is :py:const:`FILETYPE_PEM`. - - -.. py:method:: Context.set_tlsext_servername_callback(callback) - - Specify a one-argument callable to use as the TLS extension server name - callback. When a connection using the server name extension is made using - this context, the callback will be invoked with the :py:class:`Connection` - instance. - - .. versionadded:: 0.13 - +.. autoclass:: OpenSSL.SSL.Context + :members: .. _openssl-session: @@ -487,323 +257,8 @@ Connection objects Connection objects have the following methods: -.. py:method:: Connection.accept() - - Call the :py:meth:`accept` method of the underlying socket and set up SSL on the - returned socket, using the Context object supplied to this Connection object at - creation. Returns a pair *(conn, address)*. where *conn* is the new - Connection object created, and *address* is as returned by the socket's - :py:meth:`accept`. - - -.. py:method:: Connection.bind(address) - - Call the :py:meth:`bind` method of the underlying socket. - - -.. py:method:: Connection.close() - - Call the :py:meth:`close` method of the underlying socket. Note: If you want - correct SSL closure, you need to call the :py:meth:`shutdown` method first. - - -.. py:method:: Connection.connect(address) - - Call the :py:meth:`connect` method of the underlying socket and set up SSL on the - socket, using the Context object supplied to this Connection object at - creation. - - -.. py:method:: Connection.connect_ex(address) - - Call the :py:meth:`connect_ex` method of the underlying socket and set up SSL on - the socket, using the Context object supplied to this Connection object at - creation. Note that if the :py:meth:`connect_ex` method of the socket doesn't - return 0, SSL won't be initialized. - - -.. py:method:: Connection.do_handshake() - - Perform an SSL handshake (usually called after :py:meth:`renegotiate` or one of - :py:meth:`set_accept_state` or :py:meth:`set_accept_state`). This can raise the - same exceptions as :py:meth:`send` and :py:meth:`recv`. - - -.. py:method:: Connection.fileno() - - Retrieve the file descriptor number for the underlying socket. - - -.. py:method:: Connection.listen(backlog) - - Call the :py:meth:`listen` method of the underlying socket. - - -.. py:method:: Connection.get_app_data() - - Retrieve application data as set by :py:meth:`set_app_data`. - - -.. py:method:: Connection.get_cipher_list() - - Retrieve the list of ciphers used by the Connection object. WARNING: This API - has changed. It used to take an optional parameter and just return a string, - but not it returns the entire list in one go. - - -.. py:method:: Connection.get_client_ca_list() - - Retrieve the list of preferred client certificate issuers sent by the server - as :py:class:`OpenSSL.crypto.X509Name` objects. - - If this is a client :py:class:`Connection`, the list will be empty until the - connection with the server is established. - - If this is a server :py:class:`Connection`, return the list of certificate - authorities that will be sent or has been sent to the client, as controlled - by this :py:class:`Connection`'s :py:class:`Context`. - - .. versionadded:: 0.10 - - -.. py:method:: Connection.get_context() - - Retrieve the Context object associated with this Connection. - - -.. py:method:: Connection.set_context(context) - - Specify a replacement Context object for this Connection. - - -.. py:method:: Connection.get_peer_certificate() - - Retrieve the other side's certificate (if any) - - -.. py:method:: Connection.get_peer_cert_chain() - - Retrieve the tuple of the other side's certificate chain (if any) - - -.. py:method:: Connection.getpeername() - - Call the :py:meth:`getpeername` method of the underlying socket. - - -.. py:method:: Connection.getsockname() - - Call the :py:meth:`getsockname` method of the underlying socket. - - -.. py:method:: Connection.getsockopt(level, optname[, buflen]) - - Call the :py:meth:`getsockopt` method of the underlying socket. - - -.. py:method:: Connection.pending() - - Retrieve the number of bytes that can be safely read from the SSL buffer - (**not** the underlying transport buffer). - - -.. py:method:: Connection.recv(bufsize) - - Receive data from the Connection. The return value is a string representing the - data received. The maximum amount of data to be received at once, is specified - by *bufsize*. - - -.. py:method:: Connection.bio_write(bytes) - - If the Connection was created with a memory BIO, this method can be used to add - bytes to the read end of that memory BIO. The Connection can then read the - bytes (for example, in response to a call to :py:meth:`recv`). - - -.. py:method:: Connection.renegotiate() - - Renegotiate the SSL session. Call this if you wish to change cipher suites or - anything like that. - - -.. py:method:: Connection.send(string) - - Send the *string* data to the Connection. - - -.. py:method:: Connection.bio_read(bufsize) - - If the Connection was created with a memory BIO, this method can be used to - read bytes from the write end of that memory BIO. Many Connection methods will - add bytes which must be read in this manner or the buffer will eventually fill - up and the Connection will be able to take no further actions. - - -.. py:method:: Connection.sendall(string) - - Send all of the *string* data to the Connection. This calls :py:meth:`send` - repeatedly until all data is sent. If an error occurs, it's impossible to tell - how much data has been sent. - - -.. py:method:: Connection.set_accept_state() - - Set the connection to work in server mode. The handshake will be handled - automatically by read/write. - - -.. py:method:: Connection.set_app_data(data) - - Associate *data* with this Connection object. *data* can be retrieved - later using the :py:meth:`get_app_data` method. - - -.. py:method:: Connection.set_connect_state() - - Set the connection to work in client mode. The handshake will be handled - automatically by read/write. - - -.. py:method:: Connection.setblocking(flag) - - Call the :py:meth:`setblocking` method of the underlying socket. - - -.. py:method:: Connection.setsockopt(level, optname, value) - - Call the :py:meth:`setsockopt` method of the underlying socket. - - -.. py:method:: Connection.shutdown() - - Send the shutdown message to the Connection. Returns true if the shutdown - message exchange is completed and false otherwise (in which case you call - :py:meth:`recv` or :py:meth:`send` when the connection becomes - readable/writeable. - - -.. py:method:: Connection.get_shutdown() - - Get the shutdown state of the Connection. Returns a bitvector of either or - both of *SENT_SHUTDOWN* and *RECEIVED_SHUTDOWN*. - - -.. py:method:: Connection.set_shutdown(state) - - Set the shutdown state of the Connection. *state* is a bitvector of - either or both of *SENT_SHUTDOWN* and *RECEIVED_SHUTDOWN*. - - -.. py:method:: Connection.sock_shutdown(how) - - Call the :py:meth:`shutdown` method of the underlying socket. - - -.. py:method:: Connection.bio_shutdown() - - If the Connection was created with a memory BIO, this method can be used to - indicate that *end of file* has been reached on the read end of that memory - BIO. - - -.. py:method:: Connection.state_string() - - Retrieve a verbose string detailing the state of the Connection. - - -.. py:method:: Connection.client_random() - - Retrieve the random value used with the client hello message. - - -.. py:method:: Connection.server_random() - - Retrieve the random value used with the server hello message. - - -.. py:method:: Connection.master_key() - - Retrieve the value of the master key for this session. - - -.. py:method:: Connection.want_read() - - Checks if more data has to be read from the transport layer to complete an - operation. - - -.. py:method:: Connection.want_write() - - Checks if there is data to write to the transport layer to complete an - operation. - - -.. py:method:: Connection.set_tlsext_host_name(name) - - Specify the byte string to send as the server name in the client hello message. - - .. versionadded:: 0.13 - - -.. py:method:: Connection.get_servername() - - Get the value of the server name received in the client hello message. - - .. versionadded:: 0.13 - - -.. py:method:: Connection.get_session() - - Get a :py:class:`Session` instance representing the SSL session in use by - the connection, or :py:obj:`None` if there is no session. - - .. versionadded:: 0.14 - - -.. py:method:: Connection.set_session(session) - - Set a new SSL session (using a :py:class:`Session` instance) to be used by - the connection. - - .. versionadded:: 0.14 - - -.. py:method:: Connection.get_finished() - - Obtain latest TLS Finished message that we sent, or :py:obj:`None` if - handshake is not completed. - - .. versionadded:: 0.15 - - -.. py:method:: Connection.get_peer_finished() - - Obtain latest TLS Finished message that we expected from peer, or - :py:obj:`None` if handshake is not completed. - - .. versionadded:: 0.15 - - -.. py:method:: Connection.get_cipher_name() - - Obtain the name of the currently used cipher. - - .. versionadded:: 0.15 - - -.. py:method:: Connection.get_cipher_bits() - - Obtain the number of secret bits of the currently used cipher. - - .. versionadded:: 0.15 - - -.. py:method:: Connection.get_cipher_version() - - Obtain the protocol name of the currently used cipher. - - .. versionadded:: 0.15 +.. autoclass:: OpenSSL.SSL.Connection + :members: .. Rubric:: Footnotes diff --git a/doc/backward-compatibility.rst b/doc/backward-compatibility.rst new file mode 100644 index 000000000..b945e1cad --- /dev/null +++ b/doc/backward-compatibility.rst @@ -0,0 +1,18 @@ +Backward Compatibility +====================== + +pyOpenSSL has a very strong backward compatibility policy. +Generally speaking, you shouldn't ever be afraid of updating. + +If breaking changes are needed do be done, they are: + +#. …announced in the :doc:`changelog`. +#. …the old behavior raises a :exc:`DeprecationWarning` for a year. +#. …are done with another announcement in the :doc:`changelog`. + +Versioning Policy +================= + +pyOpenSSL follows `CalVer `_ in `YY.MINOR.MICRO` format. +Unlike SemVer, major versions represent the year, and are not indicative of +breaking changes. diff --git a/doc/changelog.rst b/doc/changelog.rst new file mode 100644 index 000000000..565b0521d --- /dev/null +++ b/doc/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst diff --git a/doc/conf.py b/doc/conf.py index d9c9a6781..aa83ac834 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- # # pyOpenSSL documentation build configuration file, created by # sphinx-quickstart on Sat Jul 16 07:12:22 2011. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its parent dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -11,7 +10,32 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import codecs +import os +import re +import sys + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def read_file(*parts): + """ + Build an absolute path from *parts* and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + with codecs.open(os.path.join(HERE, *parts), "rb", "ascii") as f: + return f.read() + + +def find_version(*file_paths): + version_file = read_file(*file_paths) + version_match = re.search( + r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M + ) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") + DOC_DIR = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, os.path.abspath(os.path.join(DOC_DIR, ".."))) @@ -19,201 +43,213 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +# -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.0' +needs_sphinx = "1.0" -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", +] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'pyOpenSSL' -copyright = u'2011, Jean-Paul Calderone' +project = "pyOpenSSL" +authors = "The pyOpenSSL developers" +copyright = "2001 " + authors # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.14' +version = find_version("..", "src", "OpenSSL", "version.py") # The full version, including alpha/beta/rc tags. -release = '0.14' +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# The reST default role (used for this markup `text`) to use for all documents. +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +if os.environ.get("READTHEDOCS", None) == "True": + html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") + + if "html_context" not in globals(): + html_context = {} + html_context["READTHEDOCS"] = True + +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'pyOpenSSLdoc' +htmlhelp_basename = "pyOpenSSLdoc" -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (source start file, target name, title, author, documentclass [howto/manual]) latex_documents = [ - ('index', 'pyOpenSSL.tex', u'pyOpenSSL Documentation', - u'Jean-Paul Calderone', 'manual'), + ("index", "pyOpenSSL.tex", "pyOpenSSL Documentation", authors, "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True -# -- Options for manual page output -------------------------------------------- +# -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'pyopenssl', u'pyOpenSSL Documentation', - [u'Jean-Paul Calderone'], 1) -] +man_pages = [("index", "pyopenssl", "pyOpenSSL Documentation", [authors], 1)] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "cryptography": ("https://cryptography.io/en/latest/", None), +} diff --git a/doc/index.rst b/doc/index.rst index e4a5a236d..2d64d3b1e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,26 +1,40 @@ +===================================== Welcome to pyOpenSSL's documentation! ===================================== -.. topic:: Abstract +Release v\ |release| (:doc:`What's new? `). - This module is a rather thin wrapper around (a subset of) the OpenSSL library. - With thin wrapper I mean that a lot of the object methods do nothing more than - calling a corresponding function in the OpenSSL library. +pyOpenSSL is a rather thin wrapper around (a subset of) the OpenSSL library. +With thin wrapper we mean that a lot of the object methods do nothing more than +calling a corresponding function in the OpenSSL library. Contents: +========= .. toctree:: - :maxdepth: 3 + :maxdepth: 2 introduction + install api internals + + +Meta +---- + +.. toctree:: + :maxdepth: 1 + + backward-compatibility + changelog + + Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/doc/install.rst b/doc/install.rst new file mode 100644 index 000000000..a23a75e3e --- /dev/null +++ b/doc/install.rst @@ -0,0 +1 @@ +.. include:: ../INSTALL.rst diff --git a/doc/introduction.rst b/doc/introduction.rst index c29f80c8d..cc1cfed72 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -1,17 +1,32 @@ .. _intro: +============ Introduction ============ -The reason pyOpenSSL was created is that the SSL support in the socket module in -Python 2.1 (the contemporary version of Python when the pyOpenSSL project was -begun) was severely limited. Other OpenSSL wrappers for Python at the time were -also limited, though in different ways. Unfortunately, Python's standard -library SSL support has remained weak, although other packages (such as -`M2Crypto `_) -have made great advances and now equal or exceed pyOpenSSL's functionality. - -The reason pyOpenSSL continues to be maintained is that there is a significant -user community around it, as well as a large amount of software which depends on -it. It is a great benefit to many people for pyOpenSSL to continue to exist and -advance. + +History +======= + +pyOpenSSL was originally created by Martin Sjögren because the SSL support in the standard library in Python 2.1 (the contemporary version of Python when the pyOpenSSL project was begun) was severely limited. +Other OpenSSL wrappers for Python at the time were also limited, though in different ways. + +Later it was maintained by `Jean-Paul Calderone`_ who among other things managed to make pyOpenSSL a pure Python project which the current maintainers are *very* grateful for. + +Over the time the standard library's ``ssl`` module improved, never reaching the completeness of pyOpenSSL's API coverage. +pyOpenSSL remains the only choice for full-featured TLS code in Python versions 3.9+ and PyPy_. + + +Development +=========== + +pyOpenSSL is collaboratively developed by the Python Cryptography Authority (PyCA_) that also maintains the low-level bindings called cryptography_. + + +.. include:: ../CONTRIBUTING.rst + + +.. _Jean-Paul Calderone: https://github.com/exarkun +.. _PyPy: http://pypy.org +.. _PyCA: https://github.com/pyca +.. _cryptography: https://github.com/pyca/cryptography diff --git a/examples/README b/examples/README deleted file mode 100644 index ecf95e5d6..000000000 --- a/examples/README +++ /dev/null @@ -1,48 +0,0 @@ -I've finally gotten around to writing some examples :-) - -They aren't many, but at least it's something. If you write any, feel free to -send them to me and I will add themn. - - -certgen.py - Certificate generation module -========================================== - -Example module with three functions: - createKeyPair - Create a public/private key pair - createCertRequest - Create a certificate request - createCertificate - Create a certificate given a cert request -In fact, I created the certificates and keys in the 'simple' directory with -the script mk_simple_certs.py - - -simple - Simple client/server example -===================================== - -Start the server with - python server.py PORT -and start clients with - python client.py HOST PORT - -The server is a simple echo server, anything a client sends, it sends back. - - -proxy.py - Example of an SSL-enabled proxy -========================================== - -The proxy example demonstrate how to use set_connect_state to start -talking SSL over an already connected socket. - -Usage: python proxy.py server[:port] proxy[:port] - -Contributed by Mihai Ibanescu - - -SecureXMLRPCServer.py - SSL-enabled version of SimpleXMLRPCServer -================================================================= - -This acts exactly like SimpleXMLRPCServer from the standard python library, -but uses secure connections. The technique and classes should work for any -SocketServer style server. However, the code has not been extensively tested. - -Contributed by Michal Wallace - diff --git a/examples/SecureXMLRPCServer.py b/examples/SecureXMLRPCServer.py deleted file mode 100644 index 757b49c04..000000000 --- a/examples/SecureXMLRPCServer.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -SecureXMLRPCServer module using pyOpenSSL 0.5 -Written 0907.2002 -by Michal Wallace -http://www.sabren.net/ - -This acts exactly like SimpleXMLRPCServer -from the standard python library, but -uses secure connections. The technique -and classes should work for any SocketServer -style server. However, the code has not -been extensively tested. - -This code is in the public domain. -It is provided AS-IS WITH NO WARRANTY WHATSOEVER. -""" -import SocketServer -import os, socket -import SimpleXMLRPCServer -from OpenSSL import SSL - -class SSLWrapper: - """ - This whole class exists just to filter out a parameter - passed in to the shutdown() method in SimpleXMLRPC.doPOST() - """ - def __init__(self, conn): - """ - Connection is not yet a new-style class, - so I'm making a proxy instead of subclassing. - """ - self.__dict__["conn"] = conn - def __getattr__(self,name): - return getattr(self.__dict__["conn"], name) - def __setattr__(self,name, value): - setattr(self.__dict__["conn"], name, value) - def shutdown(self, how=1): - """ - SimpleXMLRpcServer.doPOST calls shutdown(1), - and Connection.shutdown() doesn't take - an argument. So we just discard the argument. - """ - self.__dict__["conn"].shutdown() - def accept(self): - """ - This is the other part of the shutdown() workaround. - Since servers create new sockets, we have to infect - them with our magic. :) - """ - c, a = self.__dict__["conn"].accept() - return (SSLWrapper(c), a) - - - -class SecureTCPServer(SocketServer.TCPServer): - """ - Just like TCPServer, but use a socket. - This really ought to let you specify the key and certificate files. - """ - def __init__(self, server_address, RequestHandlerClass): - SocketServer.BaseServer.__init__(self, server_address, RequestHandlerClass) - - ## Same as normal, but make it secure: - ctx = SSL.Context(SSL.SSLv23_METHOD) - ctx.set_options(SSL.OP_NO_SSLv2) - - dir = os.curdir - ctx.use_privatekey_file (os.path.join(dir, 'server.pkey')) - ctx.use_certificate_file(os.path.join(dir, 'server.cert')) - - self.socket = SSLWrapper(SSL.Connection(ctx, socket.socket(self.address_family, - self.socket_type))) - self.server_bind() - self.server_activate() - - -class SecureXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): - def setup(self): - """ - We need to use socket._fileobject Because SSL.Connection - doesn't have a 'dup'. Not exactly sure WHY this is, but - this is backed up by comments in socket.py and SSL/connection.c - """ - self.connection = self.request # for doPOST - self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) - self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) - - -class SecureXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer, SecureTCPServer): - def __init__(self, addr, - requestHandler=SecureXMLRPCRequestHandler, - logRequests=1): - """ - This is the exact same code as SimpleXMLRPCServer.__init__ - except it calls SecureTCPServer.__init__ instead of plain - old TCPServer.__init__ - """ - self.funcs = {} - self.logRequests = logRequests - self.instance = None - SecureTCPServer.__init__(self, addr, requestHandler) - diff --git a/examples/certgen.py b/examples/certgen.py deleted file mode 100644 index f1572357d..000000000 --- a/examples/certgen.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: latin-1 -*- -# -# Copyright (C) AB Strakt -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. - -""" -Certificate generation module. -""" - -from OpenSSL import crypto - -TYPE_RSA = crypto.TYPE_RSA -TYPE_DSA = crypto.TYPE_DSA - -def createKeyPair(type, bits): - """ - Create a public/private key pair. - - Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA - bits - Number of bits to use in the key - Returns: The public/private key pair in a PKey object - """ - pkey = crypto.PKey() - pkey.generate_key(type, bits) - return pkey - -def createCertRequest(pkey, digest="md5", **name): - """ - Create a certificate request. - - Arguments: pkey - The key to associate with the request - digest - Digestion method to use for signing, default is md5 - **name - The name of the subject of the request, possible - arguments are: - C - Country name - ST - State or province name - L - Locality name - O - Organization name - OU - Organizational unit name - CN - Common name - emailAddress - E-mail address - Returns: The certificate request in an X509Req object - """ - req = crypto.X509Req() - subj = req.get_subject() - - for (key,value) in name.items(): - setattr(subj, key, value) - - req.set_pubkey(pkey) - req.sign(pkey, digest) - return req - -def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter), digest="md5"): - """ - Generate a certificate given a certificate request. - - Arguments: req - Certificate reqeust to use - issuerCert - The certificate of the issuer - issuerKey - The private key of the issuer - serial - Serial number for the certificate - notBefore - Timestamp (relative to now) when the certificate - starts being valid - notAfter - Timestamp (relative to now) when the certificate - stops being valid - digest - Digest method to use for signing, default is md5 - Returns: The signed certificate in an X509 object - """ - cert = crypto.X509() - cert.set_serial_number(serial) - cert.gmtime_adj_notBefore(notBefore) - cert.gmtime_adj_notAfter(notAfter) - cert.set_issuer(issuerCert.get_subject()) - cert.set_subject(req.get_subject()) - cert.set_pubkey(req.get_pubkey()) - cert.sign(issuerKey, digest) - return cert - diff --git a/examples/mk_simple_certs.py b/examples/mk_simple_certs.py deleted file mode 100644 index 9dfdd2ed5..000000000 --- a/examples/mk_simple_certs.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Create certificates and private keys for the 'simple' example. -""" - -from OpenSSL import crypto -from certgen import * # yes yes, I know, I'm lazy -cakey = createKeyPair(TYPE_RSA, 1024) -careq = createCertRequest(cakey, CN='Certificate Authority') -cacert = createCertificate(careq, (careq, cakey), 0, (0, 60*60*24*365*5)) # five years -open('simple/CA.pkey', 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, cakey)) -open('simple/CA.cert', 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cacert)) -for (fname, cname) in [('client', 'Simple Client'), ('server', 'Simple Server')]: - pkey = createKeyPair(TYPE_RSA, 1024) - req = createCertRequest(pkey, CN=cname) - cert = createCertificate(req, (cacert, cakey), 1, (0, 60*60*24*365*5)) # five years - open('simple/%s.pkey' % (fname,), 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) - open('simple/%s.cert' % (fname,), 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) diff --git a/examples/proxy.py b/examples/proxy.py deleted file mode 100644 index b094864ab..000000000 --- a/examples/proxy.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python -# -# This script demostrates how one can use pyOpenSSL to speak SSL over an HTTP -# proxy -# The challenge here is to start talking SSL over an already connected socket -# -# Author: Mihai Ibanescu -# -# $Id: proxy.py,v 1.2 2004/07/22 12:01:25 martin Exp $ - -import sys, socket, string -from OpenSSL import SSL - -def usage(exit_code=0): - print "Usage: %s server[:port] proxy[:port]" % sys.argv[0] - print " Connects SSL to the specified server (port 443 by default)" - print " using the specified proxy (port 8080 by default)" - sys.exit(exit_code) - -def main(): - # Command-line processing - if len(sys.argv) != 3: - usage(-1) - - server, proxy = sys.argv[1:3] - - run(split_host(server, 443), split_host(proxy, 8080)) - -def split_host(hostname, default_port=80): - a = string.split(hostname, ':', 1) - if len(a) == 1: - a.append(default_port) - return a[0], int(a[1]) - - -# Connects to the server, through the proxy -def run(server, proxy): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - s.connect(proxy) - except socket.error, e: - print "Unable to connect to %s:%s %s" % (proxy[0], proxy[1], str(e)) - sys.exit(-1) - - # Use the CONNECT method to get a connection to the actual server - s.send("CONNECT %s:%s HTTP/1.0\n\n" % (server[0], server[1])) - print "Proxy response: %s" % string.strip(s.recv(1024)) - - ctx = SSL.Context(SSL.SSLv23_METHOD) - conn = SSL.Connection(ctx, s) - - # Go to client mode - conn.set_connect_state() - - # start using HTTP - - conn.send("HEAD / HTTP/1.0\n\n") - print "Sever response:" - print "-" * 40 - while 1: - try: - buff = conn.recv(4096) - except SSL.ZeroReturnError: - # we're done - break - - print buff, - -if __name__ == '__main__': - main() diff --git a/examples/simple/README b/examples/simple/README deleted file mode 100644 index a0729983e..000000000 --- a/examples/simple/README +++ /dev/null @@ -1,3 +0,0 @@ -To use this example, first generate keys and certificates for both the -client and the server. You can do this with the script in the directory -above this one, mk_simple_certs.py. diff --git a/examples/simple/client.py b/examples/simple/client.py deleted file mode 100644 index 0247c676e..000000000 --- a/examples/simple/client.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: latin-1 -*- -# -# Copyright (C) AB Strakt -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. - -""" -Simple SSL client, using blocking I/O -""" - -from OpenSSL import SSL -import sys, os, select, socket - -def verify_cb(conn, cert, errnum, depth, ok): - # This obviously has to be updated - print 'Got certificate: %s' % cert.get_subject() - return ok - -if len(sys.argv) < 3: - print 'Usage: python[2] client.py HOST PORT' - sys.exit(1) - -dir = os.path.dirname(sys.argv[0]) -if dir == '': - dir = os.curdir - -# Initialize context -ctx = SSL.Context(SSL.SSLv23_METHOD) -ctx.set_verify(SSL.VERIFY_PEER, verify_cb) # Demand a certificate -ctx.use_privatekey_file (os.path.join(dir, 'client.pkey')) -ctx.use_certificate_file(os.path.join(dir, 'client.cert')) -ctx.load_verify_locations(os.path.join(dir, 'CA.cert')) - -# Set up client -sock = SSL.Connection(ctx, socket.socket(socket.AF_INET, socket.SOCK_STREAM)) -sock.connect((sys.argv[1], int(sys.argv[2]))) - -while 1: - line = sys.stdin.readline() - if line == '': - break - try: - sock.send(line) - sys.stdout.write(sock.recv(1024)) - sys.stdout.flush() - except SSL.Error: - print 'Connection died unexpectedly' - break - - -sock.shutdown() -sock.close() diff --git a/examples/simple/server.py b/examples/simple/server.py deleted file mode 100644 index 37e36ddfc..000000000 --- a/examples/simple/server.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: latin-1 -*- -# -# Copyright (C) AB Strakt -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. - -""" -Simple echo server, using nonblocking I/O -""" - -from OpenSSL import SSL -import sys, os, select, socket - - -def verify_cb(conn, cert, errnum, depth, ok): - # This obviously has to be updated - print 'Got certificate: %s' % cert.get_subject() - return ok - -if len(sys.argv) < 2: - print 'Usage: python[2] server.py PORT' - sys.exit(1) - -dir = os.path.dirname(sys.argv[0]) -if dir == '': - dir = os.curdir - -# Initialize context -ctx = SSL.Context(SSL.SSLv23_METHOD) -ctx.set_options(SSL.OP_NO_SSLv2) -ctx.set_verify(SSL.VERIFY_PEER|SSL.VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb) # Demand a certificate -ctx.use_privatekey_file (os.path.join(dir, 'server.pkey')) -ctx.use_certificate_file(os.path.join(dir, 'server.cert')) -ctx.load_verify_locations(os.path.join(dir, 'CA.cert')) - -# Set up server -server = SSL.Connection(ctx, socket.socket(socket.AF_INET, socket.SOCK_STREAM)) -server.bind(('', int(sys.argv[1]))) -server.listen(3) -server.setblocking(0) - -clients = {} -writers = {} - -def dropClient(cli, errors=None): - if errors: - print 'Client %s left unexpectedly:' % (clients[cli],) - print ' ', errors - else: - print 'Client %s left politely' % (clients[cli],) - del clients[cli] - if writers.has_key(cli): - del writers[cli] - if not errors: - cli.shutdown() - cli.close() - -while 1: - try: - r,w,_ = select.select([server]+clients.keys(), writers.keys(), []) - except: - break - - for cli in r: - if cli == server: - cli,addr = server.accept() - print 'Connection from %s' % (addr,) - clients[cli] = addr - - else: - try: - ret = cli.recv(1024) - except (SSL.WantReadError, SSL.WantWriteError, SSL.WantX509LookupError): - pass - except SSL.ZeroReturnError: - dropClient(cli) - except SSL.Error, errors: - dropClient(cli, errors) - else: - if not writers.has_key(cli): - writers[cli] = '' - writers[cli] = writers[cli] + ret - - for cli in w: - try: - ret = cli.send(writers[cli]) - except (SSL.WantReadError, SSL.WantWriteError, SSL.WantX509LookupError): - pass - except SSL.ZeroReturnError: - dropClient(cli) - except SSL.Error, errors: - dropClient(cli, errors) - else: - writers[cli] = writers[cli][ret:] - if writers[cli] == '': - del writers[cli] - -for cli in clients.keys(): - cli.close() -server.close() diff --git a/examples/sni/README b/examples/sni/README deleted file mode 100644 index 4c74eb513..000000000 --- a/examples/sni/README +++ /dev/null @@ -1,19 +0,0 @@ -This directory contains client and server examples for the "Server Name -Indication" (SNI) feature. - -Run server.py with no arguments. It will accept one client connection and -then exit. It has two certificates it can use, one for "example.invalid" -and another for "another.invalid". If a client indicates one of these names -to it, it will use the corresponding certificate for that connection (if a -client doesn't indicate a name or indicates another name, it won't try to -use any certificate). - -Run client.py with one argument, the server name to indicate. For example: - - $ python client.py example.invalid - Connecting... connected ('127.0.0.1', 8443) - Server subject is - $ - -Depending on what hostname is supplied, the server will select a different -certificate to use and the client output will be different. diff --git a/examples/sni/another.invalid.crt b/examples/sni/another.invalid.crt deleted file mode 100644 index 995e14c4e..000000000 --- a/examples/sni/another.invalid.crt +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICqTCCAhICAQEwDQYJKoZIhvcNAQEEBQAwgZwxETAPBgNVBAsTCFNlY3VyaXR5 -MRIwEAYDVQQKEwlweU9wZW5TU0wxGDAWBgNVBAMTD2Fub3RoZXIuaW52YWxpZDER -MA8GA1UECBMITmV3IFlvcmsxCzAJBgNVBAYTAlVTMSYwJAYJKoZIhvcNAQkBFhdp -bnZhbGlkQGFub3RoZXIuaW52YWxpZDERMA8GA1UEBxMITmV3IFlvcmswHhcNMTEw -NjA2MTIyMTQyWhcNMTIwNjA1MTIyMTQyWjCBnDERMA8GA1UECxMIU2VjdXJpdHkx -EjAQBgNVBAoTCXB5T3BlblNTTDEYMBYGA1UEAxMPYW5vdGhlci5pbnZhbGlkMREw -DwYDVQQIEwhOZXcgWW9yazELMAkGA1UEBhMCVVMxJjAkBgkqhkiG9w0BCQEWF2lu -dmFsaWRAYW5vdGhlci5pbnZhbGlkMREwDwYDVQQHEwhOZXcgWW9yazCBnzANBgkq -hkiG9w0BAQEFAAOBjQAwgYkCgYEA7jUOM0EnH0/bvqyQfrGlZ5ROc29JWEq3wp7/ -n96cxQ/oSf5G6rlQ5ZYnDlp44csQOY3DIq5/7cRju/Qf5cZ03YMOjzYSi4ElS0+o -3Av/VgL/ssC6Z0PfQO4+NyXIQTn+cS6P6T65AVBdqn6Z5t0eY0wkU6QznpdJ/1c2 -a7gIYnUCAwEAATANBgkqhkiG9w0BAQQFAAOBgQBqyrP1wmpTmfeZnoB7piJd+qIj -VHpCDRAZcdsxKUl/8PahjtWPMB0G5VaMwOoIGIlMxZ/LPKf44cA+QNEIXq8rohr2 -XFaA4t4X4aP7OmwQ4pa8mh4r86mP+vQU2iRJOqRYP+/gKaAqI2+ZbORZXJ7bewb5 -DTvvQRw2PRBf270h8g== ------END CERTIFICATE----- diff --git a/examples/sni/another.invalid.key b/examples/sni/another.invalid.key deleted file mode 100644 index 8d955f64b..000000000 --- a/examples/sni/another.invalid.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXgIBAAKBgQDuNQ4zQScfT9u+rJB+saVnlE5zb0lYSrfCnv+f3pzFD+hJ/kbq -uVDllicOWnjhyxA5jcMirn/txGO79B/lxnTdgw6PNhKLgSVLT6jcC/9WAv+ywLpn -Q99A7j43JchBOf5xLo/pPrkBUF2qfpnm3R5jTCRTpDOel0n/VzZruAhidQIDAQAB -AoGBAOGaJBHM8fWI17DVlKA5NVNNNaPEUW2qjjFoDuflmQpWD4UMqzOhQYm/VMwW -SYhnnr0zkw1kwUp6Bo87HX6sH37b1GeqIyp+b0Hqc+vLyiXPo0suqV23B9K8jjZ0 -6ap8h6hxpa5D1HtYKKDzWLhLJVtmtslxsvimR/CS+rmpUgBBAkEA+lJ2dXMDsUzB -xOpX8MLfQsl8XB5tx4ejmXGyNp/hmRFqFi38FFemJXX1YC3wL5jbQ2Ltz9rnbdnG -Xb/IWrn25QJBAPOcPua6xiNTWW5519JGaNgWdYnUgbj/ib8waLoElHp5Hl5DLuYX -y8U96Xl/wAE4aQnp5R/PS75tYrKZo79z9FECQQDALk1J8IpWNbLSRoRLkKEtulji -tG3d8VH1/WcwLuFZzhfffWB6Eay6N+yx8bLkJ/u2qZ4gpVRmbvqvgQ0GMp3NAkBE -FFczzeCPgLyOdjiNSCYGtYgVg7DZDXjmWFX8HkmMTIrjFu1lWiMVNS8pSD1VWflo -zte8Ywcs6Y7akLtFRtdxAkEA346J1/Zqtibez2TcjzCK+s9Ihwta23ZN2YTjo60o -sDZ5AVJwyLa7VFEzO/e9v2ytD7k9fCJjHcxIWIe8zj0dYA== ------END RSA PRIVATE KEY----- diff --git a/examples/sni/client.py b/examples/sni/client.py deleted file mode 100644 index 5b936718a..000000000 --- a/examples/sni/client.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. - -if __name__ == '__main__': - import client - raise SystemExit(client.main()) - -from sys import argv, stdout -from socket import socket - -from OpenSSL.SSL import TLSv1_METHOD, Context, Connection - -def main(): - """ - Connect to an SNI-enabled server and request a specific hostname, specified - by argv[1], of it. - """ - if len(argv) < 2: - print 'Usage: %s ' % (argv[0],) - return 1 - - client = socket() - - print 'Connecting...', - stdout.flush() - client.connect(('127.0.0.1', 8443)) - print 'connected', client.getpeername() - - client_ssl = Connection(Context(TLSv1_METHOD), client) - client_ssl.set_connect_state() - client_ssl.set_tlsext_host_name(argv[1]) - client_ssl.do_handshake() - print 'Server subject is', client_ssl.get_peer_certificate().get_subject() - client_ssl.close() - diff --git a/examples/sni/example.invalid.crt b/examples/sni/example.invalid.crt deleted file mode 100644 index b0cabac8a..000000000 --- a/examples/sni/example.invalid.crt +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICqTCCAhICAQEwDQYJKoZIhvcNAQEEBQAwgZwxETAPBgNVBAsTCFNlY3VyaXR5 -MRIwEAYDVQQKEwlweU9wZW5TU0wxGDAWBgNVBAMTD2V4YW1wbGUuaW52YWxpZDER -MA8GA1UECBMITmV3IFlvcmsxCzAJBgNVBAYTAlVTMSYwJAYJKoZIhvcNAQkBFhdp -bnZhbGlkQGV4YW1wbGUuaW52YWxpZDERMA8GA1UEBxMITmV3IFlvcmswHhcNMTEw -NjA2MTIyMTMzWhcNMTIwNjA1MTIyMTMzWjCBnDERMA8GA1UECxMIU2VjdXJpdHkx -EjAQBgNVBAoTCXB5T3BlblNTTDEYMBYGA1UEAxMPZXhhbXBsZS5pbnZhbGlkMREw -DwYDVQQIEwhOZXcgWW9yazELMAkGA1UEBhMCVVMxJjAkBgkqhkiG9w0BCQEWF2lu -dmFsaWRAZXhhbXBsZS5pbnZhbGlkMREwDwYDVQQHEwhOZXcgWW9yazCBnzANBgkq -hkiG9w0BAQEFAAOBjQAwgYkCgYEAwmLucR0IXvoGTOfzb2WJlHis2s/FFJfmYAKd -hq9bs+XzPeAPG0VQqAsy+om1gBOb8KPGtSet2SeNc25FU+QuwAza8uws7EaxD9b9 -CcarIh2X5LMcmiI/p34FuVGUSVsfc4QCTYFWGA0Mrw4jz9sGGeSEmTjVRnc3uAix -31orKScCAwEAATANBgkqhkiG9w0BAQQFAAOBgQBxm8Qta5wYFmQ3l3EAne9+HaQ5 -gPStgox6STmyOGfRkybSePgOeKftOasaXpKboiNg6PJEkaFEnl9epNwS+8PIjQqv -mPiZdlrNIfw+YVWpqgcTAIzkhYFH0K4v6d5Wn2adNgd5KbrxYOjsr2w0ixQEtdW/ -+z1x/ngjc08EPqOIPQ== ------END CERTIFICATE----- diff --git a/examples/sni/example.invalid.key b/examples/sni/example.invalid.key deleted file mode 100644 index 192e346f2..000000000 --- a/examples/sni/example.invalid.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQDCYu5xHQhe+gZM5/NvZYmUeKzaz8UUl+ZgAp2Gr1uz5fM94A8b -RVCoCzL6ibWAE5vwo8a1J63ZJ41zbkVT5C7ADNry7CzsRrEP1v0JxqsiHZfksxya -Ij+nfgW5UZRJWx9zhAJNgVYYDQyvDiPP2wYZ5ISZONVGdze4CLHfWispJwIDAQAB -AoGBAL8L8qNTUHXgL68ITRZP6g71J5YKm/zoafA0wdOsp2lA+Hb4roAz+Nif4SOh -krPlEd9JZ7OF4vRJTlmDqDmSS2qY7hJuZpdrdvhdxaPGeX4uftC43thEzxLxPQHd -gCCxugbGJOHChjMPk06oC0w1q70ex3gWmki82Jt/5INV6Z6RAkEA4km0s0RvbVmW -AT12PROplCRE86eJNlLCVp2TJNl0LPZe5uWqaZZ8wBvfFd1PXEk/Qcpj4IotMZ5M -1Ai4zw2+6QJBANvo6R5yLRrY8/7YKw9Y/1bbSRLhGYok2Ur4fFz64G28wA1VI3yS -uXrJ7NjTVykfrBq59WEfh3a15P9g/TMAPY8CQQDdW3Z9iqtpj6IScnowgwR22wfs -RW4PCuP6cMhY2rMvrI3nVrDd+wzrrBgNPmF8iFZt2Drdkq1lBVJodGO8f9jJAj9O -K3yyVeOyp2wUKsMjsX8SYOCY1Ws+r9qNy8ZpRsSAPZgHJTx4C6/i9eQ7LuTMuXV0 -CqYu4AZHLGE6Zj+a4XsCQQC8Ken471EXuahfPcKTzsphuZnYZkoVUsFUxJFfqG+S -8k2Jo/4c+2NyyvVXhXu2at8kmu45c92BrCTXIvLEwtnn ------END RSA PRIVATE KEY----- diff --git a/examples/sni/server.py b/examples/sni/server.py deleted file mode 100644 index 873841681..000000000 --- a/examples/sni/server.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. - -if __name__ == '__main__': - import server - raise SystemExit(server.main()) - -from sys import stdout -from socket import SOL_SOCKET, SO_REUSEADDR, socket - -from OpenSSL.crypto import FILETYPE_PEM, load_privatekey, load_certificate -from OpenSSL.SSL import TLSv1_METHOD, Context, Connection - -def load(domain): - crt = open(domain + ".crt") - key = open(domain + ".key") - result = ( - load_privatekey(FILETYPE_PEM, key.read()), - load_certificate(FILETYPE_PEM, crt.read())) - crt.close() - key.close() - return result - - -def main(): - """ - Run an SNI-enabled server which selects between a few certificates in a - C{dict} based on the handshake request it receives from a client. - """ - port = socket() - port.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) - port.bind(('', 8443)) - port.listen(3) - - print 'Accepting...', - stdout.flush() - server, addr = port.accept() - print 'accepted', addr - - server_context = Context(TLSv1_METHOD) - server_context.set_tlsext_servername_callback(pick_certificate) - - server_ssl = Connection(server_context, server) - server_ssl.set_accept_state() - server_ssl.do_handshake() - server.close() - - -certificates = { - "example.invalid": load("example.invalid"), - "another.invalid": load("another.invalid"), - } - - -def pick_certificate(connection): - try: - key, cert = certificates[connection.get_servername()] - except KeyError: - pass - else: - new_context = Context(TLSv1_METHOD) - new_context.use_privatekey(key) - new_context.use_certificate(cert) - connection.set_context(new_context) diff --git a/leakcheck/context-info-callback.py b/leakcheck/context-info-callback.py deleted file mode 100644 index 6a3925c96..000000000 --- a/leakcheck/context-info-callback.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. -# -# Stress tester for thread-related bugs in global_info_callback in -# src/ssl/context.c. In 0.7 and earlier, this will somewhat reliably -# segfault or abort after a few dozen to a few thousand iterations on an SMP -# machine (generally not on a UP machine) due to uses of Python/C API -# without holding the GIL. - -from itertools import count -from threading import Thread -from socket import socket - -from OpenSSL.SSL import Context, TLSv1_METHOD, Connection, WantReadError -from OpenSSL.crypto import FILETYPE_PEM, load_certificate, load_privatekey - -cleartextPrivateKeyPEM = ( - "-----BEGIN RSA PRIVATE KEY-----\n" - "MIICXAIBAAKBgQDaemNe1syksAbFFpF3aoOrZ18vB/IQNZrAjFqXPv9iieJm7+Tc\n" - "g+lA/v0qmoEKrpT2xfwxXmvZwBNM4ZhyRC3DPIFEyJV7/3IA1p5iuMY/GJI1VIgn\n" - "aikQCnrsyxtaRpsMBeZRniaVzcUJ+XnEdFGEjlo+k0xlwfVclDEMwgpXAQIDAQAB\n" - "AoGBALi0a7pMQqqgnriVAdpBVJveQtxSDVWi2/gZMKVZfzNheuSnv4amhtaKPKJ+\n" - "CMZtHkcazsE2IFvxRN/kgato9H3gJqq8nq2CkdpdLNVKBoxiCtkLfutdY4SQLtoY\n" - "USN7exk131pchsAJXYlR6mCW+ZP+E523cNwpPgsyKxVbmXSBAkEA9470fy2W0jFM\n" - "taZFslpntKSzbvn6JmdtjtvWrM1bBaeeqFiGBuQFYg46VaCUaeRWYw02jmYAsDYh\n" - "ZQavmXThaQJBAOHtlAQ0IJJEiMZr6vtVPH32fmbthSv1AUSYPzKqdlQrUnOXPQXu\n" - "z70cFoLG1TvPF5rBxbOkbQ/s8/ka5ZjPfdkCQCeC7YsO36+UpsWnUCBzRXITh4AC\n" - "7eYLQ/U1KUJTVF/GrQ/5cQrQgftwgecAxi9Qfmk4xqhbp2h4e0QAmS5I9WECQH02\n" - "0QwrX8nxFeTytr8pFGezj4a4KVCdb2B3CL+p3f70K7RIo9d/7b6frJI6ZL/LHQf2\n" - "UP4pKRDkgKsVDx7MELECQGm072/Z7vmb03h/uE95IYJOgY4nfmYs0QKA9Is18wUz\n" - "DpjfE33p0Ha6GO1VZRIQoqE24F8o5oimy3BEjryFuw4=\n" - "-----END RSA PRIVATE KEY-----\n") - - -cleartextCertificatePEM = ( - "-----BEGIN CERTIFICATE-----\n" - "MIICfTCCAeYCAQEwDQYJKoZIhvcNAQEEBQAwgYYxCzAJBgNVBAYTAlVTMRkwFwYD\n" - "VQQDExBweW9wZW5zc2wuc2YubmV0MREwDwYDVQQHEwhOZXcgWW9yazESMBAGA1UE\n" - "ChMJUHlPcGVuU1NMMREwDwYDVQQIEwhOZXcgWW9yazEQMA4GCSqGSIb3DQEJARYB\n" - "IDEQMA4GA1UECxMHVGVzdGluZzAeFw0wODAzMjUxOTA0MTNaFw0wOTAzMjUxOTA0\n" - "MTNaMIGGMQswCQYDVQQGEwJVUzEZMBcGA1UEAxMQcHlvcGVuc3NsLnNmLm5ldDER\n" - "MA8GA1UEBxMITmV3IFlvcmsxEjAQBgNVBAoTCVB5T3BlblNTTDERMA8GA1UECBMI\n" - "TmV3IFlvcmsxEDAOBgkqhkiG9w0BCQEWASAxEDAOBgNVBAsTB1Rlc3RpbmcwgZ8w\n" - "DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANp6Y17WzKSwBsUWkXdqg6tnXy8H8hA1\n" - "msCMWpc+/2KJ4mbv5NyD6UD+/SqagQqulPbF/DFea9nAE0zhmHJELcM8gUTIlXv/\n" - "cgDWnmK4xj8YkjVUiCdqKRAKeuzLG1pGmwwF5lGeJpXNxQn5ecR0UYSOWj6TTGXB\n" - "9VyUMQzCClcBAgMBAAEwDQYJKoZIhvcNAQEEBQADgYEAmm0Vzvv1O91WLl2LnF2P\n" - "q55LJdOnJbCCXIgxLdoVmvYAz1ZJq1eGKgKWI5QLgxiSzJLEU7KK//aVfiZzoCd5\n" - "RipBiEEMEV4eAY317bHPwPP+4Bj9t0l8AsDLseC5vLRHgxrLEu3bn08DYx6imB5Q\n" - "UBj849/xpszEM7BhwKE0GiQ=\n" - "-----END CERTIFICATE-----\n") - -count = count() -def go(): - port = socket() - port.bind(('', 0)) - port.listen(1) - - called = [] - def info(conn, where, ret): - print count.next() - called.append(None) - context = Context(TLSv1_METHOD) - context.set_info_callback(info) - context.use_certificate( - load_certificate(FILETYPE_PEM, cleartextCertificatePEM)) - context.use_privatekey( - load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)) - - while 1: - client = socket() - client.setblocking(False) - client.connect_ex(port.getsockname()) - - clientSSL = Connection(Context(TLSv1_METHOD), client) - clientSSL.set_connect_state() - - server, ignored = port.accept() - server.setblocking(False) - - serverSSL = Connection(context, server) - serverSSL.set_accept_state() - - del called[:] - while not called: - for ssl in clientSSL, serverSSL: - try: - ssl.do_handshake() - except WantReadError: - pass - - -threads = [Thread(target=go, args=()) for i in xrange(2)] -for th in threads: - th.start() -for th in threads: - th.join() diff --git a/leakcheck/context-passphrase-callback.py b/leakcheck/context-passphrase-callback.py deleted file mode 100644 index ba71655da..000000000 --- a/leakcheck/context-passphrase-callback.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. -# -# Stress tester for thread-related bugs in global_passphrase_callback in -# src/ssl/context.c. In 0.7 and earlier, this will somewhat reliably -# segfault or abort after a few dozen to a few thousand iterations on an SMP -# machine (generally not on a UP machine) due to uses of Python/C API -# without holding the GIL. - -from itertools import count -from threading import Thread - -from OpenSSL.SSL import Context, TLSv1_METHOD -from OpenSSL.crypto import TYPE_RSA, FILETYPE_PEM, PKey, dump_privatekey - -k = PKey() -k.generate_key(TYPE_RSA, 128) -file('pkey.pem', 'w').write(dump_privatekey(FILETYPE_PEM, k, "blowfish", "foobar")) - -count = count() -def go(): - def cb(a, b, c): - print count.next() - return "foobar" - c = Context(TLSv1_METHOD) - c.set_passwd_cb(cb) - while 1: - c.use_privatekey_file('pkey.pem') - -threads = [Thread(target=go, args=()) for i in xrange(2)] -for th in threads: - th.start() -for th in threads: - th.join() diff --git a/leakcheck/context-verify-callback.py b/leakcheck/context-verify-callback.py deleted file mode 100644 index 0ae586b2b..000000000 --- a/leakcheck/context-verify-callback.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. -# -# Stress tester for thread-related bugs in global_verify_callback in -# src/ssl/context.c. This will reliably segfault if context.c isn't a -# PyThreadState management technique which is compatible with the approach used -# by ssl.c. - - -from itertools import count -from threading import Thread -from socket import socket - -from OpenSSL.SSL import Context, TLSv1_METHOD, VERIFY_PEER, Connection, WantReadError -from OpenSSL.crypto import FILETYPE_PEM, load_certificate, load_privatekey - -cleartextPrivateKeyPEM = ( - "-----BEGIN RSA PRIVATE KEY-----\n" - "MIICXAIBAAKBgQDaemNe1syksAbFFpF3aoOrZ18vB/IQNZrAjFqXPv9iieJm7+Tc\n" - "g+lA/v0qmoEKrpT2xfwxXmvZwBNM4ZhyRC3DPIFEyJV7/3IA1p5iuMY/GJI1VIgn\n" - "aikQCnrsyxtaRpsMBeZRniaVzcUJ+XnEdFGEjlo+k0xlwfVclDEMwgpXAQIDAQAB\n" - "AoGBALi0a7pMQqqgnriVAdpBVJveQtxSDVWi2/gZMKVZfzNheuSnv4amhtaKPKJ+\n" - "CMZtHkcazsE2IFvxRN/kgato9H3gJqq8nq2CkdpdLNVKBoxiCtkLfutdY4SQLtoY\n" - "USN7exk131pchsAJXYlR6mCW+ZP+E523cNwpPgsyKxVbmXSBAkEA9470fy2W0jFM\n" - "taZFslpntKSzbvn6JmdtjtvWrM1bBaeeqFiGBuQFYg46VaCUaeRWYw02jmYAsDYh\n" - "ZQavmXThaQJBAOHtlAQ0IJJEiMZr6vtVPH32fmbthSv1AUSYPzKqdlQrUnOXPQXu\n" - "z70cFoLG1TvPF5rBxbOkbQ/s8/ka5ZjPfdkCQCeC7YsO36+UpsWnUCBzRXITh4AC\n" - "7eYLQ/U1KUJTVF/GrQ/5cQrQgftwgecAxi9Qfmk4xqhbp2h4e0QAmS5I9WECQH02\n" - "0QwrX8nxFeTytr8pFGezj4a4KVCdb2B3CL+p3f70K7RIo9d/7b6frJI6ZL/LHQf2\n" - "UP4pKRDkgKsVDx7MELECQGm072/Z7vmb03h/uE95IYJOgY4nfmYs0QKA9Is18wUz\n" - "DpjfE33p0Ha6GO1VZRIQoqE24F8o5oimy3BEjryFuw4=\n" - "-----END RSA PRIVATE KEY-----\n") - - -cleartextCertificatePEM = ( - "-----BEGIN CERTIFICATE-----\n" - "MIICfTCCAeYCAQEwDQYJKoZIhvcNAQEEBQAwgYYxCzAJBgNVBAYTAlVTMRkwFwYD\n" - "VQQDExBweW9wZW5zc2wuc2YubmV0MREwDwYDVQQHEwhOZXcgWW9yazESMBAGA1UE\n" - "ChMJUHlPcGVuU1NMMREwDwYDVQQIEwhOZXcgWW9yazEQMA4GCSqGSIb3DQEJARYB\n" - "IDEQMA4GA1UECxMHVGVzdGluZzAeFw0wODAzMjUxOTA0MTNaFw0wOTAzMjUxOTA0\n" - "MTNaMIGGMQswCQYDVQQGEwJVUzEZMBcGA1UEAxMQcHlvcGVuc3NsLnNmLm5ldDER\n" - "MA8GA1UEBxMITmV3IFlvcmsxEjAQBgNVBAoTCVB5T3BlblNTTDERMA8GA1UECBMI\n" - "TmV3IFlvcmsxEDAOBgkqhkiG9w0BCQEWASAxEDAOBgNVBAsTB1Rlc3RpbmcwgZ8w\n" - "DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANp6Y17WzKSwBsUWkXdqg6tnXy8H8hA1\n" - "msCMWpc+/2KJ4mbv5NyD6UD+/SqagQqulPbF/DFea9nAE0zhmHJELcM8gUTIlXv/\n" - "cgDWnmK4xj8YkjVUiCdqKRAKeuzLG1pGmwwF5lGeJpXNxQn5ecR0UYSOWj6TTGXB\n" - "9VyUMQzCClcBAgMBAAEwDQYJKoZIhvcNAQEEBQADgYEAmm0Vzvv1O91WLl2LnF2P\n" - "q55LJdOnJbCCXIgxLdoVmvYAz1ZJq1eGKgKWI5QLgxiSzJLEU7KK//aVfiZzoCd5\n" - "RipBiEEMEV4eAY317bHPwPP+4Bj9t0l8AsDLseC5vLRHgxrLEu3bn08DYx6imB5Q\n" - "UBj849/xpszEM7BhwKE0GiQ=\n" - "-----END CERTIFICATE-----\n") - -count = count() -def go(): - port = socket() - port.bind(('', 0)) - port.listen(1) - - called = [] - def info(*args): - print count.next() - called.append(None) - return 1 - context = Context(TLSv1_METHOD) - context.set_verify(VERIFY_PEER, info) - context.use_certificate( - load_certificate(FILETYPE_PEM, cleartextCertificatePEM)) - context.use_privatekey( - load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)) - - while 1: - client = socket() - client.setblocking(False) - client.connect_ex(port.getsockname()) - - clientSSL = Connection(context, client) - clientSSL.set_connect_state() - - server, ignored = port.accept() - server.setblocking(False) - - serverSSL = Connection(context, server) - serverSSL.set_accept_state() - - del called[:] - while not called: - for ssl in clientSSL, serverSSL: - try: - ssl.send('foo') - except WantReadError, e: - pass - - -threads = [Thread(target=go, args=()) for i in xrange(2)] -for th in threads: - th.start() -for th in threads: - th.join() - diff --git a/leakcheck/crypto.py b/leakcheck/crypto.py deleted file mode 100644 index ca79b7ccc..000000000 --- a/leakcheck/crypto.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. - -import sys - -from OpenSSL.crypto import ( - FILETYPE_PEM, TYPE_DSA, Error, PKey, X509, load_privatekey, CRL, Revoked, - get_elliptic_curves, _X509_REVOKED_dup) - -from OpenSSL._util import lib as _lib - - - -class BaseChecker(object): - def __init__(self, iterations): - self.iterations = iterations - - - -class Checker_X509_get_pubkey(BaseChecker): - """ - Leak checks for L{X509.get_pubkey}. - """ - def check_exception(self): - """ - Call the method repeatedly such that it will raise an exception. - """ - for i in xrange(self.iterations): - cert = X509() - try: - cert.get_pubkey() - except Error: - pass - - - def check_success(self): - """ - Call the method repeatedly such that it will return a PKey object. - """ - small = xrange(3) - for i in xrange(self.iterations): - key = PKey() - key.generate_key(TYPE_DSA, 256) - for i in small: - cert = X509() - cert.set_pubkey(key) - for i in small: - cert.get_pubkey() - - - -class Checker_load_privatekey(BaseChecker): - """ - Leak checks for :py:obj:`load_privatekey`. - """ - ENCRYPTED_PEM = """\ ------BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: BF-CBC,3763C340F9B5A1D0 - -a/DO10mLjHLCAOG8/Hc5Lbuh3pfjvcTZiCexShP+tupkp0VxW2YbZjML8uoXrpA6 -fSPUo7cEC+r96GjV03ZIVhjmsxxesdWMpfkzXRpG8rUbWEW2KcCJWdSX8bEkuNW3 -uvAXdXZwiOrm56ANDo/48gj27GcLwnlA8ld39+ylAzkUJ1tcMVzzTjfcyd6BMFpR -Yjg23ikseug6iWEsZQormdl0ITdYzmFpM+YYsG7kmmmi4UjCEYfb9zFaqJn+WZT2 -qXxmo2ZPFzmEVkuB46mf5GCqMwLRN2QTbIZX2+Dljj1Hfo5erf5jROewE/yzcTwO -FCB5K3c2kkTv2KjcCAimjxkE+SBKfHg35W0wB0AWkXpVFO5W/TbHg4tqtkpt/KMn -/MPnSxvYr/vEqYMfW4Y83c45iqK0Cyr2pwY60lcn8Kk= ------END RSA PRIVATE KEY----- -""" - def check_load_privatekey_callback(self): - """ - Call the function with an encrypted PEM and a passphrase callback. - """ - for i in xrange(self.iterations * 10): - load_privatekey( - FILETYPE_PEM, self.ENCRYPTED_PEM, lambda *args: "hello, secret") - - - def check_load_privatekey_callback_incorrect(self): - """ - Call the function with an encrypted PEM and a passphrase callback which - returns the wrong passphrase. - """ - for i in xrange(self.iterations * 10): - try: - load_privatekey( - FILETYPE_PEM, self.ENCRYPTED_PEM, - lambda *args: "hello, public") - except Error: - pass - - - def check_load_privatekey_callback_wrong_type(self): - """ - Call the function with an encrypted PEM and a passphrase callback which - returns a non-string. - """ - for i in xrange(self.iterations * 10): - try: - load_privatekey( - FILETYPE_PEM, self.ENCRYPTED_PEM, - lambda *args: {}) - except ValueError: - pass - - - -class Checker_CRL(BaseChecker): - """ - Leak checks for L{CRL.add_revoked} and L{CRL.get_revoked}. - """ - def check_add_revoked(self): - """ - Call the add_revoked method repeatedly on an empty CRL. - """ - for i in xrange(self.iterations * 200): - CRL().add_revoked(Revoked()) - - - def check_get_revoked(self): - """ - Create a CRL object with 100 Revoked objects, then call the - get_revoked method repeatedly. - """ - crl = CRL() - for i in xrange(100): - crl.add_revoked(Revoked()) - for i in xrange(self.iterations): - crl.get_revoked() - - - -class Checker_X509_REVOKED_dup(BaseChecker): - """ - Leak checks for :py:obj:`_X509_REVOKED_dup`. - """ - def check_X509_REVOKED_dup(self): - """ - Copy an empty Revoked object repeatedly. The copy is not garbage - collected, therefore it needs to be manually freed. - """ - for i in xrange(self.iterations * 100): - revoked_copy = _X509_REVOKED_dup(Revoked()._revoked) - _lib.X509_REVOKED_free(revoked_copy) - - - -class Checker_EllipticCurve(BaseChecker): - """ - Leak checks for :py:obj:`_EllipticCurve`. - """ - def check_to_EC_KEY(self): - """ - Repeatedly create an EC_KEY* from an :py:obj:`_EllipticCurve`. The - structure should be automatically garbage collected. - """ - curves = get_elliptic_curves() - if curves: - curve = next(iter(curves)) - for i in xrange(self.iterations * 1000): - curve._to_EC_KEY() - - -def vmsize(): - return [x for x in file('/proc/self/status').readlines() if 'VmSize' in x] - - -def main(iterations='1000'): - iterations = int(iterations) - for klass in globals(): - if klass.startswith('Checker_'): - klass = globals()[klass] - print klass - checker = klass(iterations) - for meth in dir(checker): - if meth.startswith('check_'): - print '\t', meth, vmsize(), '...', - getattr(checker, meth)() - print vmsize() - - -if __name__ == '__main__': - main(*sys.argv[1:]) diff --git a/leakcheck/dhparam.pem b/leakcheck/dhparam.pem deleted file mode 100644 index 9d33a4a8b..000000000 --- a/leakcheck/dhparam.pem +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN DH PARAMETERS----- -MEYCQQDM2LbvAjF5ahXHOUdDR09Vw/7kxjF/euWhNKBqUQQYT7FDSAMCCMq+Jhno -BKxWEDhlxR1Q1VZ4H/NVTAGtWai7AgEC ------END DH PARAMETERS----- diff --git a/leakcheck/thread-crash.py b/leakcheck/thread-crash.py deleted file mode 100644 index a1ebbdd19..000000000 --- a/leakcheck/thread-crash.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. -# -# Stress tester for thread-related bugs in ssl_Connection_send and -# ssl_Connection_recv in src/ssl/connection.c for usage of a single -# Connection object simultaneously in multiple threads. In 0.7 and earlier, -# this will somewhat reliably cause Python to abort with a "tstate mix-up" -# almost immediately, due to the incorrect sharing between threads of the -# `tstate` field of the connection object. - - -from socket import socket -from threading import Thread - -from OpenSSL.SSL import Connection, Context, TLSv1_METHOD - -def send(conn): - while 1: - for i in xrange(1024 * 32): - conn.send('x') - print 'Sent 32KB on', hex(id(conn)) - - -def recv(conn): - while 1: - for i in xrange(1024 * 64): - conn.recv(1) - print 'Received 64KB on', hex(id(conn)) - - -def main(): - port = socket() - port.bind(('', 0)) - port.listen(5) - - client = socket() - client.setblocking(False) - client.connect_ex(port.getsockname()) - client.setblocking(True) - - server = port.accept()[0] - - clientCtx = Context(TLSv1_METHOD) - clientCtx.set_cipher_list('ALL:ADH') - clientCtx.load_tmp_dh('dhparam.pem') - - sslClient = Connection(clientCtx, client) - sslClient.set_connect_state() - - serverCtx = Context(TLSv1_METHOD) - serverCtx.set_cipher_list('ALL:ADH') - serverCtx.load_tmp_dh('dhparam.pem') - - sslServer = Connection(serverCtx, server) - sslServer.set_accept_state() - - t1 = Thread(target=send, args=(sslClient,)) - t2 = Thread(target=send, args=(sslServer,)) - t3 = Thread(target=recv, args=(sslClient,)) - t4 = Thread(target=recv, args=(sslServer,)) - - t1.start() - t2.start() - t3.start() - t4.start() - t1.join() - t2.join() - t3.join() - t4.join() - -main() diff --git a/leakcheck/thread-key-gen.py b/leakcheck/thread-key-gen.py deleted file mode 100644 index 62e1a5808..000000000 --- a/leakcheck/thread-key-gen.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (C) Jean-Paul Calderone -# See LICENSE for details. -# -# Stress tester for thread-related bugs in RSA and DSA key generation. 0.12 and -# older held the GIL during these operations. Subsequent versions release it -# during them. - -from threading import Thread - -from OpenSSL.crypto import TYPE_RSA, TYPE_DSA, PKey - -def generate_rsa(): - keys = [] - for i in range(100): - key = PKey() - key.generate_key(TYPE_RSA, 1024) - keys.append(key) - -def generate_dsa(): - keys = [] - for i in range(100): - key = PKey() - key.generate_key(TYPE_DSA, 512) - keys.append(key) - - -def main(): - threads = [] - for i in range(3): - t = Thread(target=generate_rsa, args=()) - threads.append(t) - t = Thread(target=generate_dsa, args=()) - threads.append(t) - - for t in threads: - t.start() - -main() diff --git a/memdbg.py b/memdbg.py deleted file mode 100644 index 324d0fde0..000000000 --- a/memdbg.py +++ /dev/null @@ -1,82 +0,0 @@ -import sys -sys.modules['ssl'] = None -sys.modules['_hashlib'] = None - - -import traceback - -from cffi import api as _api -_ffi = _api.FFI() -_ffi.cdef( - """ - void *malloc(size_t size); - void free(void *ptr); - void *realloc(void *ptr, size_t size); - - int CRYPTO_set_mem_functions(void *(*m)(size_t),void *(*r)(void *,size_t), void (*f)(void *)); - - int backtrace(void **buffer, int size); - char **backtrace_symbols(void *const *buffer, int size); - void backtrace_symbols_fd(void *const *buffer, int size, int fd); - """) -_api = _ffi.verify( - """ - #include - #include - #include - """, libraries=["crypto"]) -C = _ffi.dlopen(None) - -verbose = False - -def log(s): - if verbose: - print(s) - -def _backtrace(): - buf = _ffi.new("void*[]", 64) - result = _api.backtrace(buf, len(buf)) - strings = _api.backtrace_symbols(buf, result) - stack = [_ffi.string(strings[i]) for i in range(result)] - C.free(strings) - return stack - - -@_ffi.callback("void*(*)(size_t)") -def malloc(n): - memory = C.malloc(n) - python_stack = traceback.extract_stack(limit=3) - c_stack = _backtrace() - heap[memory] = [(n, python_stack, c_stack)] - log("malloc(%d) -> %s" % (n, memory)) - return memory - - -@_ffi.callback("void*(*)(void*, size_t)") -def realloc(p, n): - memory = C.realloc(p, n) - old = heap.pop(p) - - python_stack = traceback.extract_stack(limit=3) - c_stack = _backtrace() - - old.append((n, python_stack, c_stack)) - heap[memory] = old - log("realloc(0x%x, %d) -> %s" % (int(_ffi.cast("int", p)), n, memory)) - return memory - - -@_ffi.callback("void(*)(void*)") -def free(p): - if p != _ffi.NULL: - C.free(p) - del heap[p] - log("free(0x%x)" % (int(_ffi.cast("int", p)),)) - - -if _api.CRYPTO_set_mem_functions(malloc, realloc, free): - log('Enabled memory debugging') - heap = {} -else: - log('Failed to enable memory debugging') - heap = None diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 000000000..aa5692b57 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,87 @@ +import nox + +nox.options.reuse_existing_virtualenvs = True +nox.options.default_venv_backend = "uv|virtualenv" + +MINIMUM_CRYPTOGRAPHY_VERSION = "49.0.0" + + +@nox.session +@nox.session(name="tests-cryptography-main") +@nox.session(name="tests-cryptography-minimum") +@nox.session(name="tests-wheel") +@nox.session(name="tests-cryptography-minimum-wheel") +@nox.session(name="tests-random-order") +def tests(session: nox.Session) -> None: + cryptography_version = None + use_wheel = False + random_order = False + + if "cryptography-main" in session.name: + cryptography_version = "main" + elif "cryptography-minimum" in session.name: + cryptography_version = "minimum" + + if "wheel" in session.name: + use_wheel = True + + if "random-order" in session.name: + random_order = True + + deps = ["coverage>=4.2"] + + if cryptography_version == "minimum": + deps.append(f"cryptography=={MINIMUM_CRYPTOGRAPHY_VERSION}") + + if random_order: + deps.append("pytest-randomly") + + extra_install_args = [] + if not use_wheel: + extra_install_args.append("--no-binary") + extra_install_args.append("cryptography") + + session.install(*deps) + session.install("-e", ".[test]", *extra_install_args) + if cryptography_version == "main": + session.install("git+https://github.com/pyca/cryptography.git") + + session.run("openssl", "version", external=True) + session.run("coverage", "run", "--parallel", "-m", "OpenSSL.debug") + session.run( + "coverage", "run", "--parallel", "-m", "pytest", "-v", *session.posargs + ) + + +@nox.session +def lint(session: nox.Session) -> None: + session.install("ruff") + session.run("ruff", "check", ".") + session.run("ruff", "format", "--check", ".") + + +@nox.session +def mypy(session: nox.Session) -> None: + session.install("-e", ".[test]") + session.install("mypy") + session.run("mypy", "src/", "tests/") + + +@nox.session(name="check-manifest") +def check_manifest(session: nox.Session) -> None: + session.install("check-manifest") + session.run("check-manifest") + + +@nox.session +def docs(session: nox.Session) -> None: + session.install("-e", ".[docs]") + session.run( + "sphinx-build", + "-W", + "-b", + "html", + "doc", + "doc/_build/html", + *session.posargs, + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..af106976a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[tool.coverage.run] +branch = true +relative_files = true +source = ["OpenSSL", "tests/"] + +[tool.coverage.paths] +source = [ + "src/OpenSSL", + "*.nox/*/lib/python*/site-packages/OpenSSL", + "*.nox/*/lib/pypy*/site-packages/OpenSSL", + "*.nox/pypy/site-packages/OpenSSL", + "*.nox\\*\\Lib\\site-packages\\OpenSSL", +] + +[tool.coverage.report] +exclude_also = ["assert False"] +show_missing = true + +[tool.mypy] +warn_unused_configs = true +strict = true +strict_bytes = true + +[[tool.mypy.overrides]] +module = "OpenSSL.*" +warn_return_any = false + +[[tool.mypy.overrides]] +module = "cryptography.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "cffi.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = ["pretend"] +ignore_missing_imports = true + +[tool.pytest.ini_options] +addopts = "-r s --strict-markers" +testpaths = ["tests"] + +[tool.ruff] +lint.select = ['E', 'F', 'I', 'W', 'UP', 'RUF'] +line-length = 79 +# Remove if/when we move setup.py python-requires metadata to pyproject.toml +target-version = "py37" + +[tool.ruff.lint.isort] +known-first-party = ["OpenSSL", "tests"] diff --git a/rpm/build_script b/rpm/build_script deleted file mode 100644 index d85e5e7c7..000000000 --- a/rpm/build_script +++ /dev/null @@ -1 +0,0 @@ -make -C doc text ps html diff --git a/runtests.py b/runtests.py deleted file mode 100644 index 13f5c4c91..000000000 --- a/runtests.py +++ /dev/null @@ -1,11 +0,0 @@ -import sys -sys.modules['ssl'] = None -sys.modules['_hashlib'] = None - -try: - import memdbg -except Exception as e: - pass - -from twisted.scripts.trial import run -run() diff --git a/setup.cfg b/setup.cfg index ec1218d9d..ba1663051 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,12 @@ -[sdist] -force_manifest=1 - -[wheel] -universal = 1 +[metadata] +# Ensure LICENSE is included in wheels. +license_file = LICENSE # bdist_rpm settings contributed by Mihai Ibanescu +# This is currently *not* actively tested. [bdist_rpm] release = 1 -build-requires = lynx openssl-devel python-devel python-sphinx +build_requires = openssl-devel python-devel python-sphinx group = Development/Libraries build_script = rpm/build_script -doc-files = doc/_build/html +doc_files = doc/_build/html diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 3d3fe04a5..971630edc --- a/setup.py +++ b/setup.py @@ -1,75 +1,105 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # -# Copyright (C) Jean-Paul Calderone 2008-2014, All rights reserved +# Copyright (C) Jean-Paul Calderone 2008-2015, All rights reserved # """ -Installation script for the OpenSSL module +Installation script for the OpenSSL package. """ -from setuptools import setup +import os +import re -# XXX Deduplicate this -__version__ = '0.14' +from setuptools import find_packages, setup -setup(name='pyOpenSSL', version=__version__, - packages = ['OpenSSL'], - package_dir = {'OpenSSL': 'OpenSSL'}, - py_modules = ['OpenSSL.__init__', - 'OpenSSL.tsafe', - 'OpenSSL.rand', - 'OpenSSL.crypto', - 'OpenSSL.SSL', - 'OpenSSL.version', - 'OpenSSL.test.__init__', - 'OpenSSL.test.util', - 'OpenSSL.test.test_crypto', - 'OpenSSL.test.test_rand', - 'OpenSSL.test.test_ssl'], - description = 'Python wrapper module around the OpenSSL library', - author = 'Jean-Paul Calderone', - author_email = 'exarkun@twistedmatrix.com', - maintainer = 'Jean-Paul Calderone', - maintainer_email = 'exarkun@twistedmatrix.com', - url = 'https://github.com/pyca/pyopenssl', - license = 'APL2', - install_requires=["cryptography>=0.4", "six>=1.5.2"], - long_description = """\ -High-level wrapper around a subset of the OpenSSL library, includes - * SSL.Connection objects, wrapping the methods of Python's portable - sockets - * Callbacks written in Python - * Extensive error-handling mechanism, mirroring OpenSSL's error codes -... and much more ;)""", - classifiers = [ - 'Development Status :: 6 - Mature', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', +HERE = os.path.abspath(os.path.dirname(__file__)) +META_PATH = os.path.join("src", "OpenSSL", "version.py") - # General classifiers to indicate "this project supports Python 2" and - # "this project supports Python 3". - 'Programming Language :: Python :: 2', - # In particular, this makes pyOpenSSL show up on - # https://pypi.python.org/pypi?:action=browse&c=533&show=all and is in - # accordance with - # http://docs.python.org/2/howto/pyporting.html#universal-bits-of-advice - 'Programming Language :: Python :: 3', - # More specific classifiers to indicate more precisely which versions - # of those languages the project supports. - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', +def read_file(*parts): + """ + Build an absolute path from *parts* and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + with open(os.path.join(HERE, *parts), encoding="utf-8", newline=None) as f: + return f.read() - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Security :: Cryptography', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: System :: Networking', + +META_FILE = read_file(META_PATH) + + +def find_meta(meta): + """ + Extract __*meta*__ from META_FILE. + """ + meta_match = re.search( + rf"^__{meta}__ = ['\"]([^'\"]*)['\"]", META_FILE, re.M + ) + if meta_match: + return meta_match.group(1) + raise RuntimeError(f"Unable to find __{meta}__ string.") + + +URI = find_meta("uri") +LONG = ( + read_file("README.rst") + + "\n\n" + + "Release Information\n" + + "===================\n\n" + + re.search( + r"(\d{2}.\d.\d \(.*?\)\n.*?)\n\n\n----\n", + read_file("CHANGELOG.rst"), + re.S, + ).group(1) + + "\n\n`Full changelog " + + "<{uri}en/stable/changelog.html>`_.\n\n" +).format(uri=URI) + + +if __name__ == "__main__": + setup( + name=find_meta("title"), + version=find_meta("version"), + description=find_meta("summary"), + long_description=LONG, + author=find_meta("author"), + author_email=find_meta("email"), + url=URI, + project_urls={ + "Source": "https://github.com/pyca/pyopenssl", + }, + license=find_meta("license"), + classifiers=[ + "Development Status :: 6 - Mature", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "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 :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Security :: Cryptography", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Networking", + ], + python_requires=">=3.9", + packages=find_packages(where="src"), + package_dir={"": "src"}, + install_requires=[ + "cryptography>=49.0.0,<50", + "typing-extensions>=4.9; python_version < '3.13'", ], - test_suite="OpenSSL") + extras_require={ + "test": ["pytest-rerunfailures", "pretend", "pytest>=3.0.1"], + "docs": [ + "sphinx!=5.2.0,!=5.2.0.post0,!=7.2.5", + "sphinx_rtd_theme", + ], + }, + ) diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py new file mode 100644 index 000000000..b7d588768 --- /dev/null +++ b/src/OpenSSL/SSL.py @@ -0,0 +1,3361 @@ +from __future__ import annotations + +import os +import socket +import sys +import typing +import warnings +from collections.abc import Sequence +from errno import errorcode +from functools import partial, wraps +from itertools import chain, count +from sys import platform +from typing import Any, Callable, Optional, TypeVar +from weakref import WeakValueDictionary + +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated + +from cryptography import x509 +from cryptography.hazmat.primitives.asymmetric import ec + +from OpenSSL._util import ( + StrOrBytesPath as _StrOrBytesPath, +) +from OpenSSL._util import ( + exception_from_error_queue as _exception_from_error_queue, +) +from OpenSSL._util import ( + ffi as _ffi, +) +from OpenSSL._util import ( + lib as _lib, +) +from OpenSSL._util import ( + make_assert as _make_assert, +) +from OpenSSL._util import ( + no_zero_allocator as _no_zero_allocator, +) +from OpenSSL._util import ( + path_bytes as _path_bytes, +) +from OpenSSL._util import ( + text_to_bytes_and_warn as _text_to_bytes_and_warn, +) +from OpenSSL.crypto import ( + FILETYPE_PEM, + X509, + PKey, + X509Name, + X509Store, + _EllipticCurve, + _PassphraseHelper, + _PrivateKey, +) + +__all__ = [ + "DTLS_CLIENT_METHOD", + "DTLS_METHOD", + "DTLS_SERVER_METHOD", + "MODE_RELEASE_BUFFERS", + "NO_OVERLAPPING_PROTOCOLS", + "OPENSSL_BUILT_ON", + "OPENSSL_CFLAGS", + "OPENSSL_DIR", + "OPENSSL_PLATFORM", + "OPENSSL_VERSION", + "OPENSSL_VERSION_NUMBER", + "OP_ALL", + "OP_CIPHER_SERVER_PREFERENCE", + "OP_DONT_INSERT_EMPTY_FRAGMENTS", + "OP_EPHEMERAL_RSA", + "OP_MICROSOFT_BIG_SSLV3_BUFFER", + "OP_MICROSOFT_SESS_ID_BUG", + "OP_MSIE_SSLV2_RSA_PADDING", + "OP_NETSCAPE_CA_DN_BUG", + "OP_NETSCAPE_CHALLENGE_BUG", + "OP_NETSCAPE_DEMO_CIPHER_CHANGE_BUG", + "OP_NETSCAPE_REUSE_CIPHER_CHANGE_BUG", + "OP_NO_COMPRESSION", + "OP_NO_QUERY_MTU", + "OP_NO_TICKET", + "OP_PKCS1_CHECK_1", + "OP_PKCS1_CHECK_2", + "OP_SINGLE_DH_USE", + "OP_SINGLE_ECDH_USE", + "OP_SSLEAY_080_CLIENT_DH_BUG", + "OP_SSLREF2_REUSE_CERT_TYPE_BUG", + "OP_TLS_BLOCK_PADDING_BUG", + "OP_TLS_D5_BUG", + "OP_TLS_ROLLBACK_BUG", + "RECEIVED_SHUTDOWN", + "SENT_SHUTDOWN", + "SESS_CACHE_BOTH", + "SESS_CACHE_CLIENT", + "SESS_CACHE_NO_AUTO_CLEAR", + "SESS_CACHE_NO_INTERNAL", + "SESS_CACHE_NO_INTERNAL_LOOKUP", + "SESS_CACHE_NO_INTERNAL_STORE", + "SESS_CACHE_OFF", + "SESS_CACHE_SERVER", + "SSL3_VERSION", + "SSLEAY_BUILT_ON", + "SSLEAY_CFLAGS", + "SSLEAY_DIR", + "SSLEAY_PLATFORM", + "SSLEAY_VERSION", + "SSL_CB_ACCEPT_EXIT", + "SSL_CB_ACCEPT_LOOP", + "SSL_CB_ALERT", + "SSL_CB_CONNECT_EXIT", + "SSL_CB_CONNECT_LOOP", + "SSL_CB_EXIT", + "SSL_CB_HANDSHAKE_DONE", + "SSL_CB_HANDSHAKE_START", + "SSL_CB_LOOP", + "SSL_CB_READ", + "SSL_CB_READ_ALERT", + "SSL_CB_WRITE", + "SSL_CB_WRITE_ALERT", + "SSL_ST_ACCEPT", + "SSL_ST_CONNECT", + "SSL_ST_MASK", + "TLS1_1_VERSION", + "TLS1_2_VERSION", + "TLS1_3_VERSION", + "TLS1_VERSION", + "TLS_CLIENT_METHOD", + "TLS_METHOD", + "TLS_SERVER_METHOD", + "VERIFY_CLIENT_ONCE", + "VERIFY_FAIL_IF_NO_PEER_CERT", + "VERIFY_NONE", + "VERIFY_PEER", + "Connection", + "Context", + "Error", + "OP_NO_SSLv2", + "OP_NO_SSLv3", + "OP_NO_TLSv1", + "OP_NO_TLSv1_1", + "OP_NO_TLSv1_2", + "OP_NO_TLSv1_3", + "SSLeay_version", + "SSLv23_METHOD", + "Session", + "SysCallError", + "TLSv1_1_METHOD", + "TLSv1_2_METHOD", + "TLSv1_METHOD", + "WantReadError", + "WantWriteError", + "WantX509LookupError", + "X509VerificationCodes", + "ZeroReturnError", +] + + +OPENSSL_VERSION_NUMBER: int = _lib.OPENSSL_VERSION_NUMBER +OPENSSL_VERSION: int = _lib.OPENSSL_VERSION +OPENSSL_CFLAGS: int = _lib.OPENSSL_CFLAGS +OPENSSL_PLATFORM: int = _lib.OPENSSL_PLATFORM +OPENSSL_DIR: int = _lib.OPENSSL_DIR +OPENSSL_BUILT_ON: int = _lib.OPENSSL_BUILT_ON + +SSLEAY_VERSION = OPENSSL_VERSION +SSLEAY_CFLAGS = OPENSSL_CFLAGS +SSLEAY_PLATFORM = OPENSSL_PLATFORM +SSLEAY_DIR = OPENSSL_DIR +SSLEAY_BUILT_ON = OPENSSL_BUILT_ON + +SENT_SHUTDOWN = _lib.SSL_SENT_SHUTDOWN +RECEIVED_SHUTDOWN = _lib.SSL_RECEIVED_SHUTDOWN + +SSLv23_METHOD = 3 +TLSv1_METHOD = 4 +TLSv1_1_METHOD = 5 +TLSv1_2_METHOD = 6 +TLS_METHOD = 7 +TLS_SERVER_METHOD = 8 +TLS_CLIENT_METHOD = 9 +DTLS_METHOD = 10 +DTLS_SERVER_METHOD = 11 +DTLS_CLIENT_METHOD = 12 + +SSL3_VERSION: int = _lib.SSL3_VERSION +TLS1_VERSION: int = _lib.TLS1_VERSION +TLS1_1_VERSION: int = _lib.TLS1_1_VERSION +TLS1_2_VERSION: int = _lib.TLS1_2_VERSION +TLS1_3_VERSION: int = _lib.TLS1_3_VERSION + +OP_NO_SSLv2: int = _lib.SSL_OP_NO_SSLv2 +OP_NO_SSLv3: int = _lib.SSL_OP_NO_SSLv3 +OP_NO_TLSv1: int = _lib.SSL_OP_NO_TLSv1 +OP_NO_TLSv1_1: int = _lib.SSL_OP_NO_TLSv1_1 +OP_NO_TLSv1_2: int = _lib.SSL_OP_NO_TLSv1_2 +OP_NO_TLSv1_3: int = _lib.SSL_OP_NO_TLSv1_3 + +MODE_RELEASE_BUFFERS: int = _lib.SSL_MODE_RELEASE_BUFFERS + +OP_SINGLE_DH_USE: int = _lib.SSL_OP_SINGLE_DH_USE +OP_SINGLE_ECDH_USE: int = _lib.SSL_OP_SINGLE_ECDH_USE +OP_EPHEMERAL_RSA: int = _lib.SSL_OP_EPHEMERAL_RSA +OP_MICROSOFT_SESS_ID_BUG: int = _lib.SSL_OP_MICROSOFT_SESS_ID_BUG +OP_NETSCAPE_CHALLENGE_BUG: int = _lib.SSL_OP_NETSCAPE_CHALLENGE_BUG +OP_NETSCAPE_REUSE_CIPHER_CHANGE_BUG: int = ( + _lib.SSL_OP_NETSCAPE_REUSE_CIPHER_CHANGE_BUG +) +OP_SSLREF2_REUSE_CERT_TYPE_BUG: int = _lib.SSL_OP_SSLREF2_REUSE_CERT_TYPE_BUG +OP_MICROSOFT_BIG_SSLV3_BUFFER: int = _lib.SSL_OP_MICROSOFT_BIG_SSLV3_BUFFER +OP_MSIE_SSLV2_RSA_PADDING: int = _lib.SSL_OP_MSIE_SSLV2_RSA_PADDING +OP_SSLEAY_080_CLIENT_DH_BUG: int = _lib.SSL_OP_SSLEAY_080_CLIENT_DH_BUG +OP_TLS_D5_BUG: int = _lib.SSL_OP_TLS_D5_BUG +OP_TLS_BLOCK_PADDING_BUG: int = _lib.SSL_OP_TLS_BLOCK_PADDING_BUG +OP_DONT_INSERT_EMPTY_FRAGMENTS: int = _lib.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS +OP_CIPHER_SERVER_PREFERENCE: int = _lib.SSL_OP_CIPHER_SERVER_PREFERENCE +OP_TLS_ROLLBACK_BUG: int = _lib.SSL_OP_TLS_ROLLBACK_BUG +OP_PKCS1_CHECK_1 = _lib.SSL_OP_PKCS1_CHECK_1 +OP_PKCS1_CHECK_2: int = _lib.SSL_OP_PKCS1_CHECK_2 +OP_NETSCAPE_CA_DN_BUG: int = _lib.SSL_OP_NETSCAPE_CA_DN_BUG +OP_NETSCAPE_DEMO_CIPHER_CHANGE_BUG: int = ( + _lib.SSL_OP_NETSCAPE_DEMO_CIPHER_CHANGE_BUG +) +OP_NO_COMPRESSION: int = _lib.SSL_OP_NO_COMPRESSION + +OP_NO_QUERY_MTU: int = _lib.SSL_OP_NO_QUERY_MTU +try: + OP_COOKIE_EXCHANGE: int | None = _lib.SSL_OP_COOKIE_EXCHANGE + __all__.append("OP_COOKIE_EXCHANGE") +except AttributeError: + OP_COOKIE_EXCHANGE = None +OP_NO_TICKET: int = _lib.SSL_OP_NO_TICKET + +try: + OP_NO_RENEGOTIATION: int = _lib.SSL_OP_NO_RENEGOTIATION + __all__.append("OP_NO_RENEGOTIATION") +except AttributeError: + pass + +try: + OP_IGNORE_UNEXPECTED_EOF: int = _lib.SSL_OP_IGNORE_UNEXPECTED_EOF + __all__.append("OP_IGNORE_UNEXPECTED_EOF") +except AttributeError: + pass + +try: + OP_LEGACY_SERVER_CONNECT: int = _lib.SSL_OP_LEGACY_SERVER_CONNECT + __all__.append("OP_LEGACY_SERVER_CONNECT") +except AttributeError: + pass + +OP_ALL: int = _lib.SSL_OP_ALL + +VERIFY_PEER: int = _lib.SSL_VERIFY_PEER +VERIFY_FAIL_IF_NO_PEER_CERT: int = _lib.SSL_VERIFY_FAIL_IF_NO_PEER_CERT +VERIFY_CLIENT_ONCE: int = _lib.SSL_VERIFY_CLIENT_ONCE +VERIFY_NONE: int = _lib.SSL_VERIFY_NONE + +SESS_CACHE_OFF: int = _lib.SSL_SESS_CACHE_OFF +SESS_CACHE_CLIENT: int = _lib.SSL_SESS_CACHE_CLIENT +SESS_CACHE_SERVER: int = _lib.SSL_SESS_CACHE_SERVER +SESS_CACHE_BOTH: int = _lib.SSL_SESS_CACHE_BOTH +SESS_CACHE_NO_AUTO_CLEAR: int = _lib.SSL_SESS_CACHE_NO_AUTO_CLEAR +SESS_CACHE_NO_INTERNAL_LOOKUP: int = _lib.SSL_SESS_CACHE_NO_INTERNAL_LOOKUP +SESS_CACHE_NO_INTERNAL_STORE: int = _lib.SSL_SESS_CACHE_NO_INTERNAL_STORE +SESS_CACHE_NO_INTERNAL: int = _lib.SSL_SESS_CACHE_NO_INTERNAL + +SSL_ST_CONNECT: int = _lib.SSL_ST_CONNECT +SSL_ST_ACCEPT: int = _lib.SSL_ST_ACCEPT +SSL_ST_MASK: int = _lib.SSL_ST_MASK + +SSL_CB_LOOP: int = _lib.SSL_CB_LOOP +SSL_CB_EXIT: int = _lib.SSL_CB_EXIT +SSL_CB_READ: int = _lib.SSL_CB_READ +SSL_CB_WRITE: int = _lib.SSL_CB_WRITE +SSL_CB_ALERT: int = _lib.SSL_CB_ALERT +SSL_CB_READ_ALERT: int = _lib.SSL_CB_READ_ALERT +SSL_CB_WRITE_ALERT: int = _lib.SSL_CB_WRITE_ALERT +SSL_CB_ACCEPT_LOOP: int = _lib.SSL_CB_ACCEPT_LOOP +SSL_CB_ACCEPT_EXIT: int = _lib.SSL_CB_ACCEPT_EXIT +SSL_CB_CONNECT_LOOP: int = _lib.SSL_CB_CONNECT_LOOP +SSL_CB_CONNECT_EXIT: int = _lib.SSL_CB_CONNECT_EXIT +SSL_CB_HANDSHAKE_START: int = _lib.SSL_CB_HANDSHAKE_START +SSL_CB_HANDSHAKE_DONE: int = _lib.SSL_CB_HANDSHAKE_DONE + +_Buffer = typing.Union[bytes, bytearray, memoryview] +_T = TypeVar("_T") + + +class _NoOverlappingProtocols: + pass + + +NO_OVERLAPPING_PROTOCOLS = _NoOverlappingProtocols() + +# Callback types. +_ALPNSelectCallback = Callable[ + [ + "Connection", + typing.List[bytes], + ], + typing.Union[bytes, _NoOverlappingProtocols], +] +_CookieGenerateCallback = Callable[["Connection"], bytes] +_CookieVerifyCallback = Callable[["Connection", bytes], bool] +_OCSPClientCallback = Callable[["Connection", bytes, Optional[_T]], bool] +_OCSPServerCallback = Callable[["Connection", Optional[_T]], bytes] +_PassphraseCallback = Callable[[int, bool, Optional[_T]], bytes] +_VerifyCallback = Callable[["Connection", X509, int, int, int], bool] + + +class X509VerificationCodes: + """ + Success and error codes for X509 verification, as returned by the + underlying ``X509_STORE_CTX_get_error()`` function and passed by pyOpenSSL + to verification callback functions. + + See `OpenSSL Verification Errors + `_ + for details. + """ + + OK = _lib.X509_V_OK + ERR_UNABLE_TO_GET_ISSUER_CERT = _lib.X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT + ERR_UNABLE_TO_GET_CRL = _lib.X509_V_ERR_UNABLE_TO_GET_CRL + ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE = ( + _lib.X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE + ) + ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE = ( + _lib.X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE + ) + ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY = ( + _lib.X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY + ) + ERR_CERT_SIGNATURE_FAILURE = _lib.X509_V_ERR_CERT_SIGNATURE_FAILURE + ERR_CRL_SIGNATURE_FAILURE = _lib.X509_V_ERR_CRL_SIGNATURE_FAILURE + ERR_CERT_NOT_YET_VALID = _lib.X509_V_ERR_CERT_NOT_YET_VALID + ERR_CERT_HAS_EXPIRED = _lib.X509_V_ERR_CERT_HAS_EXPIRED + ERR_CRL_NOT_YET_VALID = _lib.X509_V_ERR_CRL_NOT_YET_VALID + ERR_CRL_HAS_EXPIRED = _lib.X509_V_ERR_CRL_HAS_EXPIRED + ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD = ( + _lib.X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD + ) + ERR_ERROR_IN_CERT_NOT_AFTER_FIELD = ( + _lib.X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD + ) + ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD = ( + _lib.X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD + ) + ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD = ( + _lib.X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD + ) + ERR_OUT_OF_MEM = _lib.X509_V_ERR_OUT_OF_MEM + ERR_DEPTH_ZERO_SELF_SIGNED_CERT = ( + _lib.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT + ) + ERR_SELF_SIGNED_CERT_IN_CHAIN = _lib.X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN + ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY = ( + _lib.X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY + ) + ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE = ( + _lib.X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE + ) + ERR_CERT_CHAIN_TOO_LONG = _lib.X509_V_ERR_CERT_CHAIN_TOO_LONG + ERR_CERT_REVOKED = _lib.X509_V_ERR_CERT_REVOKED + ERR_INVALID_CA = _lib.X509_V_ERR_INVALID_CA + ERR_PATH_LENGTH_EXCEEDED = _lib.X509_V_ERR_PATH_LENGTH_EXCEEDED + ERR_INVALID_PURPOSE = _lib.X509_V_ERR_INVALID_PURPOSE + ERR_CERT_UNTRUSTED = _lib.X509_V_ERR_CERT_UNTRUSTED + ERR_CERT_REJECTED = _lib.X509_V_ERR_CERT_REJECTED + ERR_SUBJECT_ISSUER_MISMATCH = _lib.X509_V_ERR_SUBJECT_ISSUER_MISMATCH + ERR_AKID_SKID_MISMATCH = _lib.X509_V_ERR_AKID_SKID_MISMATCH + ERR_AKID_ISSUER_SERIAL_MISMATCH = ( + _lib.X509_V_ERR_AKID_ISSUER_SERIAL_MISMATCH + ) + ERR_KEYUSAGE_NO_CERTSIGN = _lib.X509_V_ERR_KEYUSAGE_NO_CERTSIGN + ERR_UNABLE_TO_GET_CRL_ISSUER = _lib.X509_V_ERR_UNABLE_TO_GET_CRL_ISSUER + ERR_UNHANDLED_CRITICAL_EXTENSION = ( + _lib.X509_V_ERR_UNHANDLED_CRITICAL_EXTENSION + ) + ERR_KEYUSAGE_NO_CRL_SIGN = _lib.X509_V_ERR_KEYUSAGE_NO_CRL_SIGN + ERR_UNHANDLED_CRITICAL_CRL_EXTENSION = ( + _lib.X509_V_ERR_UNHANDLED_CRITICAL_CRL_EXTENSION + ) + ERR_INVALID_NON_CA = _lib.X509_V_ERR_INVALID_NON_CA + ERR_PROXY_PATH_LENGTH_EXCEEDED = _lib.X509_V_ERR_PROXY_PATH_LENGTH_EXCEEDED + ERR_KEYUSAGE_NO_DIGITAL_SIGNATURE = ( + _lib.X509_V_ERR_KEYUSAGE_NO_DIGITAL_SIGNATURE + ) + ERR_PROXY_CERTIFICATES_NOT_ALLOWED = ( + _lib.X509_V_ERR_PROXY_CERTIFICATES_NOT_ALLOWED + ) + ERR_INVALID_EXTENSION = _lib.X509_V_ERR_INVALID_EXTENSION + ERR_INVALID_POLICY_EXTENSION = _lib.X509_V_ERR_INVALID_POLICY_EXTENSION + ERR_NO_EXPLICIT_POLICY = _lib.X509_V_ERR_NO_EXPLICIT_POLICY + ERR_DIFFERENT_CRL_SCOPE = _lib.X509_V_ERR_DIFFERENT_CRL_SCOPE + ERR_UNSUPPORTED_EXTENSION_FEATURE = ( + _lib.X509_V_ERR_UNSUPPORTED_EXTENSION_FEATURE + ) + ERR_UNNESTED_RESOURCE = _lib.X509_V_ERR_UNNESTED_RESOURCE + ERR_PERMITTED_VIOLATION = _lib.X509_V_ERR_PERMITTED_VIOLATION + ERR_EXCLUDED_VIOLATION = _lib.X509_V_ERR_EXCLUDED_VIOLATION + ERR_SUBTREE_MINMAX = _lib.X509_V_ERR_SUBTREE_MINMAX + ERR_UNSUPPORTED_CONSTRAINT_TYPE = ( + _lib.X509_V_ERR_UNSUPPORTED_CONSTRAINT_TYPE + ) + ERR_UNSUPPORTED_CONSTRAINT_SYNTAX = ( + _lib.X509_V_ERR_UNSUPPORTED_CONSTRAINT_SYNTAX + ) + ERR_UNSUPPORTED_NAME_SYNTAX = _lib.X509_V_ERR_UNSUPPORTED_NAME_SYNTAX + ERR_CRL_PATH_VALIDATION_ERROR = _lib.X509_V_ERR_CRL_PATH_VALIDATION_ERROR + ERR_HOSTNAME_MISMATCH = _lib.X509_V_ERR_HOSTNAME_MISMATCH + ERR_EMAIL_MISMATCH = _lib.X509_V_ERR_EMAIL_MISMATCH + ERR_IP_ADDRESS_MISMATCH = _lib.X509_V_ERR_IP_ADDRESS_MISMATCH + ERR_APPLICATION_VERIFICATION = _lib.X509_V_ERR_APPLICATION_VERIFICATION + + +# Taken from https://golang.org/src/crypto/x509/root_linux.go +_CERTIFICATE_FILE_LOCATIONS = [ + "/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu/Gentoo etc. + "/etc/pki/tls/certs/ca-bundle.crt", # Fedora/RHEL 6 + "/etc/ssl/ca-bundle.pem", # OpenSUSE + "/etc/pki/tls/cacert.pem", # OpenELEC + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", # CentOS/RHEL 7 +] + +_CERTIFICATE_PATH_LOCATIONS = [ + "/etc/ssl/certs", # SLES10/SLES11 +] + +# These values are compared to output from cffi's ffi.string so they must be +# byte strings. +_CRYPTOGRAPHY_MANYLINUX_CA_DIR = b"/opt/pyca/cryptography/openssl/certs" +_CRYPTOGRAPHY_MANYLINUX_CA_FILE = b"/opt/pyca/cryptography/openssl/cert.pem" + + +class Error(Exception): + """ + An error occurred in an `OpenSSL.SSL` API. + """ + + +_raise_current_error = partial(_exception_from_error_queue, Error) +_openssl_assert = _make_assert(Error) + + +class WantReadError(Error): + pass + + +class WantWriteError(Error): + pass + + +class WantX509LookupError(Error): + pass + + +class ZeroReturnError(Error): + pass + + +class SysCallError(Error): + pass + + +class _CallbackExceptionHelper: + """ + A base class for wrapper classes that allow for intelligent exception + handling in OpenSSL callbacks. + + :ivar list _problems: Any exceptions that occurred while executing in a + context where they could not be raised in the normal way. Typically + this is because OpenSSL has called into some Python code and requires a + return value. The exceptions are saved to be raised later when it is + possible to do so. + """ + + def __init__(self) -> None: + self._problems: list[Exception] = [] + + def raise_if_problem(self) -> None: + """ + Raise an exception from the OpenSSL error queue or that was previously + captured whe running a callback. + """ + if self._problems: + try: + _raise_current_error() + except Error: + pass + raise self._problems.pop(0) + + +class _VerifyHelper(_CallbackExceptionHelper): + """ + Wrap a callback such that it can be used as a certificate verification + callback. + """ + + def __init__(self, callback: _VerifyCallback) -> None: + _CallbackExceptionHelper.__init__(self) + + @wraps(callback) + def wrapper(ok, store_ctx): # type: ignore[no-untyped-def] + x509 = _lib.X509_STORE_CTX_get_current_cert(store_ctx) + _lib.X509_up_ref(x509) + cert = X509._from_raw_x509_ptr(x509) + error_number = _lib.X509_STORE_CTX_get_error(store_ctx) + error_depth = _lib.X509_STORE_CTX_get_error_depth(store_ctx) + + index = _lib.SSL_get_ex_data_X509_STORE_CTX_idx() + ssl = _lib.X509_STORE_CTX_get_ex_data(store_ctx, index) + connection = Connection._reverse_mapping[ssl] + + try: + result = callback( + connection, cert, error_number, error_depth, ok + ) + except Exception as e: + self._problems.append(e) + return 0 + else: + if result: + _lib.X509_STORE_CTX_set_error(store_ctx, _lib.X509_V_OK) + return 1 + else: + return 0 + + self.callback = _ffi.callback( + "int (*)(int, X509_STORE_CTX *)", wrapper + ) + + +class _ALPNSelectHelper(_CallbackExceptionHelper): + """ + Wrap a callback such that it can be used as an ALPN selection callback. + """ + + def __init__(self, callback: _ALPNSelectCallback) -> None: + _CallbackExceptionHelper.__init__(self) + + @wraps(callback) + def wrapper(ssl, out, outlen, in_, inlen, arg): # type: ignore[no-untyped-def] + try: + conn = Connection._reverse_mapping[ssl] + + # The string passed to us is made up of multiple + # length-prefixed bytestrings. We need to split that into a + # list. + instr = _ffi.buffer(in_, inlen)[:] + protolist = [] + while instr: + encoded_len = instr[0] + proto = instr[1 : encoded_len + 1] + protolist.append(proto) + instr = instr[encoded_len + 1 :] + + # Call the callback + outbytes = callback(conn, protolist) + any_accepted = True + if outbytes is NO_OVERLAPPING_PROTOCOLS: + outbytes = b"" + any_accepted = False + elif not isinstance(outbytes, bytes): + raise TypeError( + "ALPN callback must return a bytestring or the " + "special NO_OVERLAPPING_PROTOCOLS sentinel value." + ) + + # Save our callback arguments on the connection object to make + # sure that they don't get freed before OpenSSL can use them. + # Then, return them in the appropriate output parameters. + conn._alpn_select_callback_args = [ + _ffi.new("unsigned char *", len(outbytes)), + _ffi.new("unsigned char[]", outbytes), + ] + outlen[0] = conn._alpn_select_callback_args[0][0] + out[0] = conn._alpn_select_callback_args[1] + if not any_accepted: + return _lib.SSL_TLSEXT_ERR_NOACK + return _lib.SSL_TLSEXT_ERR_OK + except Exception as e: + self._problems.append(e) + return _lib.SSL_TLSEXT_ERR_ALERT_FATAL + + self.callback = _ffi.callback( + ( + "int (*)(SSL *, unsigned char **, unsigned char *, " + "const unsigned char *, unsigned int, void *)" + ), + wrapper, + ) + + +class _OCSPServerCallbackHelper(_CallbackExceptionHelper): + """ + Wrap a callback such that it can be used as an OCSP callback for the server + side. + + Annoyingly, OpenSSL defines one OCSP callback but uses it in two different + ways. For servers, that callback is expected to retrieve some OCSP data and + hand it to OpenSSL, and may return only SSL_TLSEXT_ERR_OK, + SSL_TLSEXT_ERR_FATAL, and SSL_TLSEXT_ERR_NOACK. For clients, that callback + is expected to check the OCSP data, and returns a negative value on error, + 0 if the response is not acceptable, or positive if it is. These are + mutually exclusive return code behaviours, and they mean that we need two + helpers so that we always return an appropriate error code if the user's + code throws an exception. + + Given that we have to have two helpers anyway, these helpers are a bit more + helpery than most: specifically, they hide a few more of the OpenSSL + functions so that the user has an easier time writing these callbacks. + + This helper implements the server side. + """ + + def __init__(self, callback: _OCSPServerCallback[Any]) -> None: + _CallbackExceptionHelper.__init__(self) + + @wraps(callback) + def wrapper(ssl, cdata): # type: ignore[no-untyped-def] + try: + conn = Connection._reverse_mapping[ssl] + + # Extract the data if any was provided. + if cdata != _ffi.NULL: + data = _ffi.from_handle(cdata) + else: + data = None + + # Call the callback. + ocsp_data = callback(conn, data) + + if not isinstance(ocsp_data, bytes): + raise TypeError("OCSP callback must return a bytestring.") + + # If the OCSP data was provided, we will pass it to OpenSSL. + # However, we have an early exit here: if no OCSP data was + # provided we will just exit out and tell OpenSSL that there + # is nothing to do. + if not ocsp_data: + return 3 # SSL_TLSEXT_ERR_NOACK + + # OpenSSL takes ownership of this data and expects it to have + # been allocated by OPENSSL_malloc. + ocsp_data_length = len(ocsp_data) + data_ptr = _lib.OPENSSL_malloc(ocsp_data_length) + _ffi.buffer(data_ptr, ocsp_data_length)[:] = ocsp_data + + _lib.SSL_set_tlsext_status_ocsp_resp( + ssl, data_ptr, ocsp_data_length + ) + + return 0 + except Exception as e: + self._problems.append(e) + return 2 # SSL_TLSEXT_ERR_ALERT_FATAL + + self.callback = _ffi.callback("int (*)(SSL *, void *)", wrapper) + + +class _OCSPClientCallbackHelper(_CallbackExceptionHelper): + """ + Wrap a callback such that it can be used as an OCSP callback for the client + side. + + Annoyingly, OpenSSL defines one OCSP callback but uses it in two different + ways. For servers, that callback is expected to retrieve some OCSP data and + hand it to OpenSSL, and may return only SSL_TLSEXT_ERR_OK, + SSL_TLSEXT_ERR_FATAL, and SSL_TLSEXT_ERR_NOACK. For clients, that callback + is expected to check the OCSP data, and returns a negative value on error, + 0 if the response is not acceptable, or positive if it is. These are + mutually exclusive return code behaviours, and they mean that we need two + helpers so that we always return an appropriate error code if the user's + code throws an exception. + + Given that we have to have two helpers anyway, these helpers are a bit more + helpery than most: specifically, they hide a few more of the OpenSSL + functions so that the user has an easier time writing these callbacks. + + This helper implements the client side. + """ + + def __init__(self, callback: _OCSPClientCallback[Any]) -> None: + _CallbackExceptionHelper.__init__(self) + + @wraps(callback) + def wrapper(ssl, cdata): # type: ignore[no-untyped-def] + try: + conn = Connection._reverse_mapping[ssl] + + # Extract the data if any was provided. + if cdata != _ffi.NULL: + data = _ffi.from_handle(cdata) + else: + data = None + + # Get the OCSP data. + ocsp_ptr = _ffi.new("unsigned char **") + ocsp_len = _lib.SSL_get_tlsext_status_ocsp_resp(ssl, ocsp_ptr) + if ocsp_len < 0: + # No OCSP data. + ocsp_data = b"" + else: + # Copy the OCSP data, then pass it to the callback. + ocsp_data = _ffi.buffer(ocsp_ptr[0], ocsp_len)[:] + + valid = callback(conn, ocsp_data, data) + + # Return 1 on success or 0 on error. + return int(bool(valid)) + + except Exception as e: + self._problems.append(e) + # Return negative value if an exception is hit. + return -1 + + self.callback = _ffi.callback("int (*)(SSL *, void *)", wrapper) + + +class _CookieGenerateCallbackHelper(_CallbackExceptionHelper): + def __init__(self, callback: _CookieGenerateCallback) -> None: + _CallbackExceptionHelper.__init__(self) + + max_cookie_len = getattr(_lib, "DTLS1_COOKIE_LENGTH", 255) + + @wraps(callback) + def wrapper(ssl, out, outlen): # type: ignore[no-untyped-def] + try: + conn = Connection._reverse_mapping[ssl] + cookie = callback(conn) + if len(cookie) > max_cookie_len: + raise ValueError( + f"Cookie too long (got {len(cookie)} bytes, " + f"max {max_cookie_len})" + ) + out[0 : len(cookie)] = cookie + outlen[0] = len(cookie) + return 1 + except Exception as e: + self._problems.append(e) + # "a zero return value can be used to abort the handshake" + return 0 + + self.callback = _ffi.callback( + "int (*)(SSL *, unsigned char *, unsigned int *)", + wrapper, + ) + + +class _CookieVerifyCallbackHelper(_CallbackExceptionHelper): + def __init__(self, callback: _CookieVerifyCallback) -> None: + _CallbackExceptionHelper.__init__(self) + + @wraps(callback) + def wrapper(ssl, c_cookie, cookie_len): # type: ignore[no-untyped-def] + try: + conn = Connection._reverse_mapping[ssl] + return callback(conn, bytes(c_cookie[0:cookie_len])) + except Exception as e: + self._problems.append(e) + return 0 + + self.callback = _ffi.callback( + "int (*)(SSL *, unsigned char *, unsigned int)", + wrapper, + ) + + +def _asFileDescriptor(obj: Any) -> int: + fd = None + if not isinstance(obj, int): + meth = getattr(obj, "fileno", None) + if meth is not None: + obj = meth() + + if isinstance(obj, int): + fd = obj + + if not isinstance(fd, int): + raise TypeError("argument must be an int, or have a fileno() method.") + elif fd < 0: + raise ValueError( + f"file descriptor cannot be a negative integer ({fd:i})" + ) + + return fd + + +def OpenSSL_version(type: int) -> bytes: + """ + Return a string describing the version of OpenSSL in use. + + :param type: One of the :const:`OPENSSL_` constants defined in this module. + """ + return _ffi.string(_lib.OpenSSL_version(type)) + + +SSLeay_version = OpenSSL_version + + +def _make_requires(flag: int, error: str) -> Callable[[_T], _T]: + """ + Builds a decorator that ensures that functions that rely on OpenSSL + functions that are not present in this build raise NotImplementedError, + rather than AttributeError coming out of cryptography. + + :param flag: A cryptography flag that guards the functions, e.g. + ``Cryptography_HAS_NEXTPROTONEG``. + :param error: The string to be used in the exception if the flag is false. + """ + + def _requires_decorator(func): # type: ignore[no-untyped-def] + if not flag: + + @wraps(func) + def explode(*args, **kwargs): # type: ignore[no-untyped-def] + raise NotImplementedError(error) + + return explode + else: + return func + + return _requires_decorator + + +_requires_keylog = _make_requires( + getattr(_lib, "Cryptography_HAS_KEYLOG", 0), "Key logging not available" +) + +_requires_ssl_get0_group_name = _make_requires( + getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", 0), + "Getting group name is not supported by the linked OpenSSL version", +) + +_requires_ssl_cookie = _make_requires( + getattr(_lib, "Cryptography_HAS_SSL_COOKIE", 0), + "DTLS cookie support is not available", +) + + +class Session: + """ + A class representing an SSL session. A session defines certain connection + parameters which may be re-used to speed up the setup of subsequent + connections. + + .. versionadded:: 0.14 + """ + + _session: Any + # The Context the Connection this Session came from was using. OpenSSL + # requires that a session only be re-used with a compatible SSL_CTX, but + # doesn't verify it, so we pin the Context here and enforce identity in + # Connection.set_session. + _context: Context + + +F = TypeVar("F", bound=Callable[..., Any]) + + +def _require_not_used(f: F) -> F: + @wraps(f) + def inner(self: Context, *args: Any, **kwargs: Any) -> Any: + if self._used: + raise ValueError( + "Context has already been used to create a Connection, it " + "cannot be mutated again" + ) + return f(self, *args, **kwargs) + + return typing.cast(F, inner) + + +class Context: + """ + :class:`OpenSSL.SSL.Context` instances define the parameters for setting + up new SSL connections. + + :param method: One of TLS_METHOD, TLS_CLIENT_METHOD, TLS_SERVER_METHOD, + DTLS_METHOD, DTLS_CLIENT_METHOD, or DTLS_SERVER_METHOD. + SSLv23_METHOD, TLSv1_METHOD, etc. are deprecated and should + not be used. + """ + + _methods: typing.ClassVar[ + dict[int, tuple[Callable[[], Any], int | None]] + ] = { + SSLv23_METHOD: (_lib.TLS_method, None), + TLSv1_METHOD: (_lib.TLS_method, TLS1_VERSION), + TLSv1_1_METHOD: (_lib.TLS_method, TLS1_1_VERSION), + TLSv1_2_METHOD: (_lib.TLS_method, TLS1_2_VERSION), + TLS_METHOD: (_lib.TLS_method, None), + TLS_SERVER_METHOD: (_lib.TLS_server_method, None), + TLS_CLIENT_METHOD: (_lib.TLS_client_method, None), + DTLS_METHOD: (_lib.DTLS_method, None), + DTLS_SERVER_METHOD: (_lib.DTLS_server_method, None), + DTLS_CLIENT_METHOD: (_lib.DTLS_client_method, None), + } + + def __init__(self, method: int) -> None: + if not isinstance(method, int): + raise TypeError("method must be an integer") + + try: + method_func, version = self._methods[method] + except KeyError: + raise ValueError("No such protocol") + + method_obj = method_func() + _openssl_assert(method_obj != _ffi.NULL) + + context = _lib.SSL_CTX_new(method_obj) + _openssl_assert(context != _ffi.NULL) + context = _ffi.gc(context, _lib.SSL_CTX_free) + + self._context = context + self._used = False + self._passphrase_helper: _PassphraseHelper | None = None + self._passphrase_callback: _PassphraseCallback[Any] | None = None + self._passphrase_userdata: Any | None = None + self._verify_helper: _VerifyHelper | None = None + self._verify_callback: _VerifyCallback | None = None + self._info_callback = None + self._keylog_callback = None + self._tlsext_servername_callback = None + self._app_data = None + self._alpn_select_helper: _ALPNSelectHelper | None = None + self._alpn_select_callback: _ALPNSelectCallback | None = None + self._ocsp_helper: ( + _OCSPClientCallbackHelper | _OCSPServerCallbackHelper | None + ) = None + self._ocsp_callback: ( + _OCSPClientCallback[Any] | _OCSPServerCallback[Any] | None + ) = None + self._ocsp_data: Any | None = None + self._cookie_generate_helper: _CookieGenerateCallbackHelper | None = ( + None + ) + self._cookie_verify_helper: _CookieVerifyCallbackHelper | None = None + + self.set_mode( + _lib.SSL_MODE_ENABLE_PARTIAL_WRITE + | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + ) + if version is not None: + self.set_min_proto_version(version) + self.set_max_proto_version(version) + + @_require_not_used + def set_min_proto_version(self, version: int) -> None: + """ + Set the minimum supported protocol version. Setting the minimum + version to 0 will enable protocol versions down to the lowest version + supported by the library. + + If the underlying OpenSSL build is missing support for the selected + version, this method will raise an exception. + """ + _openssl_assert( + _lib.SSL_CTX_set_min_proto_version(self._context, version) == 1 + ) + + @_require_not_used + def set_max_proto_version(self, version: int) -> None: + """ + Set the maximum supported protocol version. Setting the maximum + version to 0 will enable protocol versions up to the highest version + supported by the library. + + If the underlying OpenSSL build is missing support for the selected + version, this method will raise an exception. + """ + _openssl_assert( + _lib.SSL_CTX_set_max_proto_version(self._context, version) == 1 + ) + + @_require_not_used + def load_verify_locations( + self, + cafile: _StrOrBytesPath | None, + capath: _StrOrBytesPath | None = None, + ) -> None: + """ + Let SSL know where we can find trusted certificates for the certificate + chain. Note that the certificates have to be in PEM format. + + If capath is passed, it must be a directory prepared using the + ``c_rehash`` tool included with OpenSSL. Either, but not both, of + *pemfile* or *capath* may be :data:`None`. + + :param cafile: In which file we can find the certificates (``bytes`` or + ``str``). + :param capath: In which directory we can find the certificates + (``bytes`` or ``str``). + + :return: None + """ + if cafile is None: + cafile = _ffi.NULL + else: + cafile = _path_bytes(cafile) + + if capath is None: + capath = _ffi.NULL + else: + capath = _path_bytes(capath) + + load_result = _lib.SSL_CTX_load_verify_locations( + self._context, cafile, capath + ) + if not load_result: + _raise_current_error() + + def _wrap_callback( + self, callback: _PassphraseCallback[_T] + ) -> _PassphraseHelper: + @wraps(callback) + def wrapper(size: int, verify: bool, userdata: Any) -> bytes: + return callback(size, verify, self._passphrase_userdata) + + return _PassphraseHelper( + FILETYPE_PEM, wrapper, more_args=True, truncate=True + ) + + @deprecated( + "Context.set_passwd_cb is deprecated. You should decrypt and load " + "your private key yourself, with cryptography's key loading APIs, " + "and then use Context.use_privatekey instead." + ) + @_require_not_used + def set_passwd_cb( + self, + callback: _PassphraseCallback[_T], + userdata: _T | None = None, + ) -> None: + """ + Set the passphrase callback. This function will be called + when a private key with a passphrase is loaded. + + :param callback: The Python callback to use. This must accept three + positional arguments. First, an integer giving the maximum length + of the passphrase it may return. If the returned passphrase is + longer than this, it will be truncated. Second, a boolean value + which will be true if the user should be prompted for the + passphrase twice and the callback should verify that the two values + supplied are equal. Third, the value given as the *userdata* + parameter to :meth:`set_passwd_cb`. The *callback* must return + a byte string. If an error occurs, *callback* should return a false + value (e.g. an empty string). + :param userdata: (optional) A Python object which will be given as + argument to the callback + :return: None + """ + if not callable(callback): + raise TypeError("callback must be callable") + + self._passphrase_helper = self._wrap_callback(callback) + self._passphrase_callback = self._passphrase_helper.callback + _lib.SSL_CTX_set_default_passwd_cb( + self._context, self._passphrase_callback + ) + self._passphrase_userdata = userdata + + @_require_not_used + def set_default_verify_paths(self) -> None: + """ + Specify that the platform provided CA certificates are to be used for + verification purposes. This method has some caveats related to the + binary wheels that cryptography (pyOpenSSL's primary dependency) ships: + + * macOS will only load certificates using this method if the user has + the ``openssl@3`` `Homebrew `_ formula installed + in the default location. + * Windows will not work. + * manylinux cryptography wheels will work on most common Linux + distributions in pyOpenSSL 17.1.0 and above. pyOpenSSL detects the + manylinux wheel and attempts to load roots via a fallback path. + + :return: None + """ + # SSL_CTX_set_default_verify_paths will attempt to load certs from + # both a cafile and capath that are set at compile time. However, + # it will first check environment variables and, if present, load + # those paths instead + set_result = _lib.SSL_CTX_set_default_verify_paths(self._context) + _openssl_assert(set_result == 1) + # After attempting to set default_verify_paths we need to know whether + # to go down the fallback path. + # First we'll check to see if any env vars have been set. If so, + # we won't try to do anything else because the user has set the path + # themselves. + if not self._check_env_vars_set("SSL_CERT_DIR", "SSL_CERT_FILE"): + default_dir = _ffi.string(_lib.X509_get_default_cert_dir()) + default_file = _ffi.string(_lib.X509_get_default_cert_file()) + # Now we check to see if the default_dir and default_file are set + # to the exact values we use in our manylinux builds. If they are + # then we know to load the fallbacks + if ( + default_dir == _CRYPTOGRAPHY_MANYLINUX_CA_DIR + and default_file == _CRYPTOGRAPHY_MANYLINUX_CA_FILE + ): + # This is manylinux, let's load our fallback paths + self._fallback_default_verify_paths( + _CERTIFICATE_FILE_LOCATIONS, _CERTIFICATE_PATH_LOCATIONS + ) + + def _check_env_vars_set(self, dir_env_var: str, file_env_var: str) -> bool: + """ + Check to see if the default cert dir/file environment vars are present. + + :return: bool + """ + return ( + os.environ.get(file_env_var) is not None + or os.environ.get(dir_env_var) is not None + ) + + def _fallback_default_verify_paths( + self, file_path: list[str], dir_path: list[str] + ) -> None: + """ + Default verify paths are based on the compiled version of OpenSSL. + However, when pyca/cryptography is compiled as a manylinux wheel + that compiled location can potentially be wrong. So, like Go, we + will try a predefined set of paths and attempt to load roots + from there. + + :return: None + """ + for cafile in file_path: + if os.path.isfile(cafile): + self.load_verify_locations(cafile) + break + + for capath in dir_path: + if os.path.isdir(capath): + self.load_verify_locations(None, capath) + break + + @_require_not_used + def use_certificate_chain_file(self, certfile: _StrOrBytesPath) -> None: + """ + Load a certificate chain from a file. + + :param certfile: The name of the certificate chain file (``bytes`` or + ``str``). Must be PEM encoded. + + :return: None + """ + certfile = _path_bytes(certfile) + + result = _lib.SSL_CTX_use_certificate_chain_file( + self._context, certfile + ) + if not result: + _raise_current_error() + + @_require_not_used + def use_certificate_file( + self, certfile: _StrOrBytesPath, filetype: int = FILETYPE_PEM + ) -> None: + """ + Load a certificate from a file + + :param certfile: The name of the certificate file (``bytes`` or + ``str``). + :param filetype: (optional) The encoding of the file, which is either + :const:`FILETYPE_PEM` or :const:`FILETYPE_ASN1`. The default is + :const:`FILETYPE_PEM`. + + :return: None + """ + certfile = _path_bytes(certfile) + if not isinstance(filetype, int): + raise TypeError("filetype must be an integer") + + use_result = _lib.SSL_CTX_use_certificate_file( + self._context, certfile, filetype + ) + if not use_result: + _raise_current_error() + + @_require_not_used + def use_certificate(self, cert: X509 | x509.Certificate) -> None: + """ + Load a certificate from a X509 object + + :param cert: The X509 object + :return: None + """ + # Mirrored at Connection.use_certificate + if not isinstance(cert, X509): + cert = X509.from_cryptography(cert) + else: + warnings.warn( + ( + "Passing pyOpenSSL X509 objects is deprecated. You " + "should use a cryptography.x509.Certificate instead." + ), + DeprecationWarning, + stacklevel=2, + ) + + use_result = _lib.SSL_CTX_use_certificate(self._context, cert._x509) + if not use_result: + _raise_current_error() + + @_require_not_used + def add_extra_chain_cert(self, certobj: X509 | x509.Certificate) -> None: + """ + Add certificate to chain + + :param certobj: The X509 certificate object to add to the chain + :return: None + """ + if not isinstance(certobj, X509): + certobj = X509.from_cryptography(certobj) + else: + warnings.warn( + ( + "Passing pyOpenSSL X509 objects is deprecated. You " + "should use a cryptography.x509.Certificate instead." + ), + DeprecationWarning, + stacklevel=2, + ) + + copy = _lib.X509_dup(certobj._x509) + add_result = _lib.SSL_CTX_add_extra_chain_cert(self._context, copy) + if not add_result: + # TODO: This is untested. + _lib.X509_free(copy) + _raise_current_error() + + def _raise_passphrase_exception(self) -> None: + if self._passphrase_helper is not None: + self._passphrase_helper.raise_if_problem(Error) + + _raise_current_error() + + @_require_not_used + def use_privatekey_file( + self, keyfile: _StrOrBytesPath, filetype: int = FILETYPE_PEM + ) -> None: + """ + Load a private key from a file + + :param keyfile: The name of the key file (``bytes`` or ``str``) + :param filetype: (optional) The encoding of the file, which is either + :const:`FILETYPE_PEM` or :const:`FILETYPE_ASN1`. The default is + :const:`FILETYPE_PEM`. + + :return: None + """ + keyfile = _path_bytes(keyfile) + + if not isinstance(filetype, int): + raise TypeError("filetype must be an integer") + + use_result = _lib.SSL_CTX_use_PrivateKey_file( + self._context, keyfile, filetype + ) + if not use_result: + self._raise_passphrase_exception() + + @_require_not_used + def use_privatekey(self, pkey: _PrivateKey | PKey) -> None: + """ + Load a private key from a PKey object + + :param pkey: The PKey object + :return: None + """ + # Mirrored at Connection.use_privatekey + if not isinstance(pkey, PKey): + pkey = PKey.from_cryptography_key(pkey) + else: + warnings.warn( + ( + "Passing pyOpenSSL PKey objects is deprecated. You " + "should use a cryptography private key instead." + ), + DeprecationWarning, + stacklevel=2, + ) + + use_result = _lib.SSL_CTX_use_PrivateKey(self._context, pkey._pkey) + if not use_result: + self._raise_passphrase_exception() + + def check_privatekey(self) -> None: + """ + Check if the private key (loaded with :meth:`use_privatekey`) matches + the certificate (loaded with :meth:`use_certificate`) + + :return: :data:`None` (raises :exc:`Error` if something's wrong) + """ + if not _lib.SSL_CTX_check_private_key(self._context): + _raise_current_error() + + @_require_not_used + def load_client_ca(self, cafile: bytes) -> None: + """ + Load the trusted certificates that will be sent to the client. Does + not actually imply any of the certificates are trusted; that must be + configured separately. + + :param bytes cafile: The path to a certificates file in PEM format. + :return: None + """ + ca_list = _lib.SSL_load_client_CA_file( + _text_to_bytes_and_warn("cafile", cafile) + ) + _openssl_assert(ca_list != _ffi.NULL) + _lib.SSL_CTX_set_client_CA_list(self._context, ca_list) + + @_require_not_used + def set_session_id(self, buf: bytes) -> None: + """ + Set the session id to *buf* within which a session can be reused for + this Context object. This is needed when doing session resumption, + because there is no way for a stored session to know which Context + object it is associated with. + + :param bytes buf: The session id. + + :returns: None + """ + buf = _text_to_bytes_and_warn("buf", buf) + _openssl_assert( + _lib.SSL_CTX_set_session_id_context(self._context, buf, len(buf)) + == 1 + ) + + @_require_not_used + def set_session_cache_mode(self, mode: int) -> int: + """ + Set the behavior of the session cache used by all connections using + this Context. The previously set mode is returned. See + :const:`SESS_CACHE_*` for details about particular modes. + + :param mode: One or more of the SESS_CACHE_* flags (combine using + bitwise or) + :returns: The previously set caching mode. + + .. versionadded:: 0.14 + """ + if not isinstance(mode, int): + raise TypeError("mode must be an integer") + + return _lib.SSL_CTX_set_session_cache_mode(self._context, mode) + + def get_session_cache_mode(self) -> int: + """ + Get the current session cache mode. + + :returns: The currently used cache mode. + + .. versionadded:: 0.14 + """ + return _lib.SSL_CTX_get_session_cache_mode(self._context) + + @_require_not_used + def set_verify( + self, mode: int, callback: _VerifyCallback | None = None + ) -> None: + """ + Set the verification flags for this Context object to *mode* and + specify that *callback* should be used for verification callbacks. + + :param mode: The verify mode, this should be one of + :const:`VERIFY_NONE` and :const:`VERIFY_PEER`. If + :const:`VERIFY_PEER` is used, *mode* can be OR:ed with + :const:`VERIFY_FAIL_IF_NO_PEER_CERT` and + :const:`VERIFY_CLIENT_ONCE` to further control the behaviour. + :param callback: The optional Python verification callback to use. + This should take five arguments: A Connection object, an X509 + object, and three integer variables, which are in turn potential + error number, error depth and return code. *callback* should + return True if verification passes and False otherwise. + If omitted, OpenSSL's default verification is used. + :return: None + + See SSL_CTX_set_verify(3SSL) for further details. + """ + if not isinstance(mode, int): + raise TypeError("mode must be an integer") + + if callback is None: + self._verify_helper = None + self._verify_callback = None + _lib.SSL_CTX_set_verify(self._context, mode, _ffi.NULL) + else: + if not callable(callback): + raise TypeError("callback must be callable") + + self._verify_helper = _VerifyHelper(callback) + self._verify_callback = self._verify_helper.callback + _lib.SSL_CTX_set_verify(self._context, mode, self._verify_callback) + + @_require_not_used + def set_verify_depth(self, depth: int) -> None: + """ + Set the maximum depth for the certificate chain verification that shall + be allowed for this Context object. + + :param depth: An integer specifying the verify depth + :return: None + """ + if not isinstance(depth, int): + raise TypeError("depth must be an integer") + + _lib.SSL_CTX_set_verify_depth(self._context, depth) + + def get_verify_mode(self) -> int: + """ + Retrieve the Context object's verify mode, as set by + :meth:`set_verify`. + + :return: The verify mode + """ + return _lib.SSL_CTX_get_verify_mode(self._context) + + def get_verify_depth(self) -> int: + """ + Retrieve the Context object's verify depth, as set by + :meth:`set_verify_depth`. + + :return: The verify depth + """ + return _lib.SSL_CTX_get_verify_depth(self._context) + + @_require_not_used + def load_tmp_dh(self, dhfile: _StrOrBytesPath) -> None: + """ + Load parameters for Ephemeral Diffie-Hellman + + :param dhfile: The file to load EDH parameters from (``bytes`` or + ``str``). + + :return: None + """ + dhfile = _path_bytes(dhfile) + + bio = _lib.BIO_new_file(dhfile, b"r") + if bio == _ffi.NULL: + _raise_current_error() + bio = _ffi.gc(bio, _lib.BIO_free) + + dh = _lib.PEM_read_bio_DHparams(bio, _ffi.NULL, _ffi.NULL, _ffi.NULL) + dh = _ffi.gc(dh, _lib.DH_free) + res = _lib.SSL_CTX_set_tmp_dh(self._context, dh) + _openssl_assert(res == 1) + + @_require_not_used + def set_tmp_ecdh(self, curve: _EllipticCurve | ec.EllipticCurve) -> None: + """ + Select a curve to use for ECDHE key exchange. + + :param curve: A curve instance from cryptography + (:class:`~cryptogragraphy.hazmat.primitives.asymmetric.ec.EllipticCurve`). + Alternatively (deprecated) a curve object from either + :meth:`OpenSSL.crypto.get_elliptic_curve` or + :meth:`OpenSSL.crypto.get_elliptic_curves`. + + :return: None + """ + + if isinstance(curve, _EllipticCurve): + warnings.warn( + ( + "Passing pyOpenSSL elliptic curves to set_tmp_ecdh is " + "deprecated. You should use cryptography's elliptic curve " + "types instead." + ), + DeprecationWarning, + stacklevel=2, + ) + _lib.SSL_CTX_set_tmp_ecdh(self._context, curve._to_EC_KEY()) + else: + name = curve.name + if name == "secp192r1": + name = "prime192v1" + elif name == "secp256r1": + name = "prime256v1" + nid = _lib.OBJ_txt2nid(name.encode()) + if nid == _lib.NID_undef: + _raise_current_error() + + ec = _lib.EC_KEY_new_by_curve_name(nid) + _openssl_assert(ec != _ffi.NULL) + ec = _ffi.gc(ec, _lib.EC_KEY_free) + _lib.SSL_CTX_set_tmp_ecdh(self._context, ec) + + @_require_not_used + def set_cipher_list(self, cipher_list: bytes) -> None: + """ + Set the list of ciphers to be used in this context. + + See the OpenSSL manual for more information (e.g. + :manpage:`ciphers(1)`). + + Note this API does not change the cipher suites used in TLS 1.3 + Use `set_tls13_ciphersuites` for that. + + :param bytes cipher_list: An OpenSSL cipher string. + :return: None + """ + cipher_list = _text_to_bytes_and_warn("cipher_list", cipher_list) + + if not isinstance(cipher_list, bytes): + raise TypeError("cipher_list must be a byte string.") + + _openssl_assert( + _lib.SSL_CTX_set_cipher_list(self._context, cipher_list) == 1 + ) + + @_require_not_used + def set_tls13_ciphersuites(self, ciphersuites: bytes) -> None: + """ + Set the list of TLS 1.3 ciphers to be used in this context. + OpenSSL maintains a separate list of TLS 1.3+ ciphers to + ciphers for TLS 1.2 and lowers. + + See the OpenSSL manual for more information (e.g. + :manpage:`ciphers(1)`). + + :param bytes ciphersuites: An OpenSSL cipher string containing + TLS 1.3+ ciphersuites. + :return: None + + .. versionadded:: 25.2.0 + """ + if not isinstance(ciphersuites, bytes): + raise TypeError("ciphersuites must be a byte string.") + + _openssl_assert( + _lib.SSL_CTX_set_ciphersuites(self._context, ciphersuites) == 1 + ) + + @deprecated( + "Context.set_client_ca_list is deprecated. X509Name support in " + "pyOpenSSL is deprecated." + ) + @_require_not_used + def set_client_ca_list( + self, certificate_authorities: Sequence[X509Name] + ) -> None: + """ + Set the list of preferred client certificate signers for this server + context. + + This list of certificate authorities will be sent to the client when + the server requests a client certificate. + + :param certificate_authorities: a sequence of X509Names. + :return: None + + .. versionadded:: 0.10 + """ + name_stack = _lib.sk_X509_NAME_new_null() + _openssl_assert(name_stack != _ffi.NULL) + + try: + for ca_name in certificate_authorities: + if not isinstance(ca_name, X509Name): + raise TypeError( + f"client CAs must be X509Name objects, not " + f"{type(ca_name).__name__} objects" + ) + copy = _lib.X509_NAME_dup(ca_name._name) + _openssl_assert(copy != _ffi.NULL) + push_result = _lib.sk_X509_NAME_push(name_stack, copy) + if not push_result: + _lib.X509_NAME_free(copy) + _raise_current_error() + except Exception: + _lib.sk_X509_NAME_free(name_stack) + raise + + _lib.SSL_CTX_set_client_CA_list(self._context, name_stack) + + @_require_not_used + def add_client_ca( + self, certificate_authority: X509 | x509.Certificate + ) -> None: + """ + Add the CA certificate to the list of preferred signers for this + context. + + The list of certificate authorities will be sent to the client when the + server requests a client certificate. + + :param certificate_authority: certificate authority's X509 certificate. + :return: None + + .. versionadded:: 0.10 + """ + if not isinstance(certificate_authority, X509): + certificate_authority = X509.from_cryptography( + certificate_authority + ) + else: + warnings.warn( + ( + "Passing pyOpenSSL X509 objects is deprecated. You " + "should use a cryptography.x509.Certificate instead." + ), + DeprecationWarning, + stacklevel=2, + ) + + add_result = _lib.SSL_CTX_add_client_CA( + self._context, certificate_authority._x509 + ) + _openssl_assert(add_result == 1) + + @_require_not_used + def set_timeout(self, timeout: int) -> None: + """ + Set the timeout for newly created sessions for this Context object to + *timeout*. The default value is 300 seconds. See the OpenSSL manual + for more information (e.g. :manpage:`SSL_CTX_set_timeout(3)`). + + :param timeout: The timeout in (whole) seconds + :return: The previous session timeout + """ + if not isinstance(timeout, int): + raise TypeError("timeout must be an integer") + + return _lib.SSL_CTX_set_timeout(self._context, timeout) + + def get_timeout(self) -> int: + """ + Retrieve session timeout, as set by :meth:`set_timeout`. The default + is 300 seconds. + + :return: The session timeout + """ + return _lib.SSL_CTX_get_timeout(self._context) + + @_require_not_used + def set_info_callback( + self, callback: Callable[[Connection, int, int], None] + ) -> None: + """ + Set the information callback to *callback*. This function will be + called from time to time during SSL handshakes. + + :param callback: The Python callback to use. This should take three + arguments: a Connection object and two integers. The first integer + specifies where in the SSL handshake the function was called, and + the other the return code from a (possibly failed) internal + function call. + :return: None + """ + + @wraps(callback) + def wrapper(ssl, where, return_code): # type: ignore[no-untyped-def] + callback(Connection._reverse_mapping[ssl], where, return_code) + + self._info_callback = _ffi.callback( + "void (*)(const SSL *, int, int)", wrapper + ) + _lib.SSL_CTX_set_info_callback(self._context, self._info_callback) + + @_requires_keylog + @_require_not_used + def set_keylog_callback( + self, callback: Callable[[Connection, bytes], None] + ) -> None: + """ + Set the TLS key logging callback to *callback*. This function will be + called whenever TLS key material is generated or received, in order + to allow applications to store this keying material for debugging + purposes. + + :param callback: The Python callback to use. This should take two + arguments: a Connection object and a bytestring that contains + the key material in the format used by NSS for its SSLKEYLOGFILE + debugging output. + :return: None + """ + + @wraps(callback) + def wrapper(ssl, line): # type: ignore[no-untyped-def] + line = _ffi.string(line) + callback(Connection._reverse_mapping[ssl], line) + + self._keylog_callback = _ffi.callback( + "void (*)(const SSL *, const char *)", wrapper + ) + _lib.SSL_CTX_set_keylog_callback(self._context, self._keylog_callback) + + def get_app_data(self) -> Any: + """ + Get the application data (supplied via :meth:`set_app_data()`) + + :return: The application data + """ + return self._app_data + + @_require_not_used + def set_app_data(self, data: Any) -> None: + """ + Set the application data (will be returned from get_app_data()) + + :param data: Any Python object + :return: None + """ + self._app_data = data + + def get_cert_store(self) -> X509Store | None: + """ + Get the certificate store for the context. This can be used to add + "trusted" certificates without using the + :meth:`load_verify_locations` method. + + :return: A X509Store object or None if it does not have one. + """ + store = _lib.SSL_CTX_get_cert_store(self._context) + if store == _ffi.NULL: + # TODO: This is untested. + return None + + pystore = X509Store.__new__(X509Store) + pystore._store = store + return pystore + + @_require_not_used + def set_options(self, options: int) -> int: + """ + Add options. Options set before are not cleared! + This method should be used with the :const:`OP_*` constants. + + :param options: The options to add. + :return: The new option bitmask. + """ + if not isinstance(options, int): + raise TypeError("options must be an integer") + + return _lib.SSL_CTX_set_options(self._context, options) + + @_require_not_used + def set_mode(self, mode: int) -> int: + """ + Add modes via bitmask. Modes set before are not cleared! This method + should be used with the :const:`MODE_*` constants. + + :param mode: The mode to add. + :return: The new mode bitmask. + """ + if not isinstance(mode, int): + raise TypeError("mode must be an integer") + + return _lib.SSL_CTX_set_mode(self._context, mode) + + @_require_not_used + def clear_mode(self, mode_to_clear: int) -> int: + """ + Modes previously set cannot be overwritten without being + cleared first. This method should be used to clear existing modes. + """ + return _lib.SSL_CTX_clear_mode(self._context, mode_to_clear) + + @_require_not_used + def set_tlsext_servername_callback( + self, callback: Callable[[Connection], None] + ) -> None: + """ + Specify a callback function to be called when clients specify a server + name. + + :param callback: The callback function. It will be invoked with one + argument, the Connection instance. + + .. versionadded:: 0.13 + """ + + @wraps(callback) + def wrapper(ssl, alert, arg): # type: ignore[no-untyped-def] + try: + callback(Connection._reverse_mapping[ssl]) + except Exception: + sys.excepthook(*sys.exc_info()) + return _lib.SSL_TLSEXT_ERR_ALERT_FATAL + return 0 + + self._tlsext_servername_callback = _ffi.callback( + "int (*)(SSL *, int *, void *)", wrapper + ) + _lib.SSL_CTX_set_tlsext_servername_callback( + self._context, self._tlsext_servername_callback + ) + + @_require_not_used + def set_tlsext_use_srtp(self, profiles: bytes) -> None: + """ + Enable support for negotiating SRTP keying material. + + :param bytes profiles: A colon delimited list of protection profile + names, like ``b'SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32'``. + :return: None + """ + if not isinstance(profiles, bytes): + raise TypeError("profiles must be a byte string.") + + _openssl_assert( + _lib.SSL_CTX_set_tlsext_use_srtp(self._context, profiles) == 0 + ) + + @_require_not_used + def set_alpn_protos(self, protos: list[bytes]) -> None: + """ + Specify the protocols that the client is prepared to speak after the + TLS connection has been negotiated using Application Layer Protocol + Negotiation. + + :param protos: A list of the protocols to be offered to the server. + This list should be a Python list of bytestrings representing the + protocols to offer, e.g. ``[b'http/1.1', b'spdy/2']``. + """ + # Different versions of OpenSSL are inconsistent about how they handle + # empty proto lists (see #1043), so we avoid the problem entirely by + # rejecting them ourselves. + if not protos: + raise ValueError("at least one protocol must be specified") + + # Take the list of protocols and join them together, prefixing them + # with their lengths. + protostr = b"".join( + chain.from_iterable((bytes((len(p),)), p) for p in protos) + ) + + # Build a C string from the list. We don't need to save this off + # because OpenSSL immediately copies the data out. + input_str = _ffi.new("unsigned char[]", protostr) + + # https://www.openssl.org/docs/man1.1.0/man3/SSL_CTX_set_alpn_protos.html: + # SSL_CTX_set_alpn_protos() and SSL_set_alpn_protos() + # return 0 on success, and non-0 on failure. + # WARNING: these functions reverse the return value convention. + _openssl_assert( + _lib.SSL_CTX_set_alpn_protos( + self._context, input_str, len(protostr) + ) + == 0 + ) + + @_require_not_used + def set_alpn_select_callback(self, callback: _ALPNSelectCallback) -> None: + """ + Specify a callback function that will be called on the server when a + client offers protocols using ALPN. + + :param callback: The callback function. It will be invoked with two + arguments: the Connection, and a list of offered protocols as + bytestrings, e.g ``[b'http/1.1', b'spdy/2']``. It can return + one of those bytestrings to indicate the chosen protocol, the + empty bytestring to terminate the TLS connection, or the + :py:obj:`NO_OVERLAPPING_PROTOCOLS` to indicate that no offered + protocol was selected, but that the connection should not be + aborted. + """ + self._alpn_select_helper = _ALPNSelectHelper(callback) + self._alpn_select_callback = self._alpn_select_helper.callback + _lib.SSL_CTX_set_alpn_select_cb( + self._context, self._alpn_select_callback, _ffi.NULL + ) + + def _set_ocsp_callback( + self, + helper: _OCSPClientCallbackHelper | _OCSPServerCallbackHelper, + data: Any | None, + ) -> None: + """ + This internal helper does the common work for + ``set_ocsp_server_callback`` and ``set_ocsp_client_callback``, which is + almost all of it. + """ + self._ocsp_helper = helper + self._ocsp_callback = helper.callback + if data is None: + self._ocsp_data = _ffi.NULL + else: + self._ocsp_data = _ffi.new_handle(data) + + rc = _lib.SSL_CTX_set_tlsext_status_cb( + self._context, self._ocsp_callback + ) + _openssl_assert(rc == 1) + rc = _lib.SSL_CTX_set_tlsext_status_arg(self._context, self._ocsp_data) + _openssl_assert(rc == 1) + + @_require_not_used + def set_ocsp_server_callback( + self, + callback: _OCSPServerCallback[_T], + data: _T | None = None, + ) -> None: + """ + Set a callback to provide OCSP data to be stapled to the TLS handshake + on the server side. + + :param callback: The callback function. It will be invoked with two + arguments: the Connection, and the optional arbitrary data you have + provided. The callback must return a bytestring that contains the + OCSP data to staple to the handshake. If no OCSP data is available + for this connection, return the empty bytestring. + :param data: Some opaque data that will be passed into the callback + function when called. This can be used to avoid needing to do + complex data lookups or to keep track of what context is being + used. This parameter is optional. + """ + helper = _OCSPServerCallbackHelper(callback) + self._set_ocsp_callback(helper, data) + + @_require_not_used + def set_ocsp_client_callback( + self, + callback: _OCSPClientCallback[_T], + data: _T | None = None, + ) -> None: + """ + Set a callback to validate OCSP data stapled to the TLS handshake on + the client side. + + :param callback: The callback function. It will be invoked with three + arguments: the Connection, a bytestring containing the stapled OCSP + assertion, and the optional arbitrary data you have provided. The + callback must return a boolean that indicates the result of + validating the OCSP data: ``True`` if the OCSP data is valid and + the certificate can be trusted, or ``False`` if either the OCSP + data is invalid or the certificate has been revoked. + :param data: Some opaque data that will be passed into the callback + function when called. This can be used to avoid needing to do + complex data lookups or to keep track of what context is being + used. This parameter is optional. + """ + helper = _OCSPClientCallbackHelper(callback) + self._set_ocsp_callback(helper, data) + + @_require_not_used + @_requires_ssl_cookie + def set_cookie_generate_callback( + self, callback: _CookieGenerateCallback + ) -> None: + self._cookie_generate_helper = _CookieGenerateCallbackHelper(callback) + _lib.SSL_CTX_set_cookie_generate_cb( + self._context, + self._cookie_generate_helper.callback, + ) + + @_require_not_used + @_requires_ssl_cookie + def set_cookie_verify_callback( + self, callback: _CookieVerifyCallback + ) -> None: + self._cookie_verify_helper = _CookieVerifyCallbackHelper(callback) + _lib.SSL_CTX_set_cookie_verify_cb( + self._context, + self._cookie_verify_helper.callback, + ) + + +class Connection: + _reverse_mapping: typing.MutableMapping[Any, Connection] = ( + WeakValueDictionary() + ) + + def __init__( + self, context: Context, socket: socket.socket | None = None + ) -> None: + """ + Create a new Connection object, using the given OpenSSL.SSL.Context + instance and socket. + + :param context: An SSL Context to use for this connection + :param socket: The socket to use for transport layer + """ + if not isinstance(context, Context): + raise TypeError("context must be a Context instance") + + context._used = True + + ssl = _lib.SSL_new(context._context) + self._ssl = _ffi.gc(ssl, _lib.SSL_free) + # We set SSL_MODE_AUTO_RETRY to handle situations where OpenSSL returns + # an SSL_ERROR_WANT_READ when processing a non-application data packet + # even though there is still data on the underlying transport. + # See https://github.com/openssl/openssl/issues/6234 for more details. + _lib.SSL_set_mode(self._ssl, _lib.SSL_MODE_AUTO_RETRY) + self._context = context + self._app_data = None + + # References to strings used for Application Layer Protocol + # Negotiation. These strings get copied at some point but it's well + # after the callback returns, so we have to hang them somewhere to + # avoid them getting freed. + self._alpn_select_callback_args: Any = None + + # Reference the verify_callback of the Context. This ensures that if + # set_verify is called again after the SSL object has been created we + # do not point to a dangling reference + self._verify_helper = context._verify_helper + self._verify_callback = context._verify_callback + + # And likewise for the cookie callbacks + self._cookie_generate_helper = context._cookie_generate_helper + self._cookie_verify_helper = context._cookie_verify_helper + + self._reverse_mapping[self._ssl] = self + + if socket is None: + self._socket = None + # Don't set up any gc for these, SSL_free will take care of them. + self._into_ssl = _lib.BIO_new(_lib.BIO_s_mem()) + _openssl_assert(self._into_ssl != _ffi.NULL) + + self._from_ssl = _lib.BIO_new(_lib.BIO_s_mem()) + _openssl_assert(self._from_ssl != _ffi.NULL) + + _lib.SSL_set_bio(self._ssl, self._into_ssl, self._from_ssl) + else: + self._into_ssl = None + self._from_ssl = None + self._socket = socket + set_result = _lib.SSL_set_fd( + self._ssl, _asFileDescriptor(self._socket) + ) + _openssl_assert(set_result == 1) + + def __getattr__(self, name: str) -> Any: + """ + Look up attributes on the wrapped socket object if they are not found + on the Connection object. + """ + if self._socket is None: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) + else: + return getattr(self._socket, name) + + def _raise_ssl_error(self, ssl: Any, result: int) -> None: + if self._context._verify_helper is not None: + self._context._verify_helper.raise_if_problem() + if self._context._alpn_select_helper is not None: + self._context._alpn_select_helper.raise_if_problem() + if self._context._ocsp_helper is not None: + self._context._ocsp_helper.raise_if_problem() + + error = _lib.SSL_get_error(ssl, result) + if error == _lib.SSL_ERROR_WANT_READ: + raise WantReadError() + elif error == _lib.SSL_ERROR_WANT_WRITE: + raise WantWriteError() + elif error == _lib.SSL_ERROR_ZERO_RETURN: + raise ZeroReturnError() + elif error == _lib.SSL_ERROR_WANT_X509_LOOKUP: + # TODO: This is untested. + raise WantX509LookupError() + elif error == _lib.SSL_ERROR_SYSCALL: + if platform == "win32": + errno = _ffi.getwinerror()[0] + else: + errno = _ffi.errno + if _lib.ERR_peek_error() == 0 or errno != 0: + if result < 0 and errno != 0: + raise SysCallError(errno, errorcode.get(errno)) + raise SysCallError(-1, "Unexpected EOF") + else: + # TODO: This is untested, but I think twisted hits it? + _raise_current_error() + elif error == _lib.SSL_ERROR_SSL and _lib.ERR_peek_error() != 0: + # In 3.0.x an unexpected EOF no longer triggers syscall error + # but we want to maintain compatibility so we check here and + # raise syscall if it is an EOF. Since we're not actually sure + # what else could raise SSL_ERROR_SSL we check for the presence + # of the OpenSSL 3 constant SSL_R_UNEXPECTED_EOF_WHILE_READING + # and if it's not present we just raise an error, which matches + # the behavior before we added this elif section + peeked_error = _lib.ERR_peek_error() + reason = _lib.ERR_GET_REASON(peeked_error) + if _lib.Cryptography_HAS_UNEXPECTED_EOF_WHILE_READING: + _openssl_assert( + reason == _lib.SSL_R_UNEXPECTED_EOF_WHILE_READING + ) + _lib.ERR_clear_error() + raise SysCallError(-1, "Unexpected EOF") + else: + _raise_current_error() + elif error == _lib.SSL_ERROR_NONE: + pass + else: + _raise_current_error() + + def get_context(self) -> Context: + """ + Retrieve the :class:`Context` object associated with this + :class:`Connection`. + """ + return self._context + + def set_context(self, context: Context) -> None: + """ + Switch this connection to a new session context. + + :param context: A :class:`Context` instance giving the new session + context to use. + """ + if not isinstance(context, Context): + raise TypeError("context must be a Context instance") + + _lib.SSL_set_SSL_CTX(self._ssl, context._context) + self._context = context + self._context._used = True + + def set_options(self, options: int) -> int: + """ + Add options. Options set before are not cleared! + This method should be used with the :const:`OP_*` constants. + + :param options: The options to add. + :return: The new option bitmask. + """ + if not isinstance(options, int): + raise TypeError("options must be an integer") + + return _lib.SSL_set_options(self._ssl, options) + + def get_servername(self) -> bytes | None: + """ + Retrieve the servername extension value if provided in the client hello + message, or None if there wasn't one. + + :return: A byte string giving the server name or :data:`None`. + + .. versionadded:: 0.13 + """ + name = _lib.SSL_get_servername( + self._ssl, _lib.TLSEXT_NAMETYPE_host_name + ) + if name == _ffi.NULL: + return None + + return _ffi.string(name) + + def set_verify( + self, mode: int, callback: _VerifyCallback | None = None + ) -> None: + """ + Override the Context object's verification flags for this specific + connection. See :py:meth:`Context.set_verify` for details. + """ + if not isinstance(mode, int): + raise TypeError("mode must be an integer") + + if callback is None: + self._verify_helper = None + self._verify_callback = None + _lib.SSL_set_verify(self._ssl, mode, _ffi.NULL) + else: + if not callable(callback): + raise TypeError("callback must be callable") + + self._verify_helper = _VerifyHelper(callback) + self._verify_callback = self._verify_helper.callback + _lib.SSL_set_verify(self._ssl, mode, self._verify_callback) + + def get_verify_mode(self) -> int: + """ + Retrieve the Connection object's verify mode, as set by + :meth:`set_verify`. + + :return: The verify mode + """ + return _lib.SSL_get_verify_mode(self._ssl) + + def use_certificate(self, cert: X509 | x509.Certificate) -> None: + """ + Load a certificate from a X509 object + + :param cert: The X509 object + :return: None + """ + # Mirrored from Context.use_certificate + if not isinstance(cert, X509): + cert = X509.from_cryptography(cert) + else: + warnings.warn( + ( + "Passing pyOpenSSL X509 objects is deprecated. You " + "should use a cryptography.x509.Certificate instead." + ), + DeprecationWarning, + stacklevel=2, + ) + + use_result = _lib.SSL_use_certificate(self._ssl, cert._x509) + if not use_result: + _raise_current_error() + + def use_privatekey(self, pkey: _PrivateKey | PKey) -> None: + """ + Load a private key from a PKey object + + :param pkey: The PKey object + :return: None + """ + # Mirrored from Context.use_privatekey + if not isinstance(pkey, PKey): + pkey = PKey.from_cryptography_key(pkey) + else: + warnings.warn( + ( + "Passing pyOpenSSL PKey objects is deprecated. You " + "should use a cryptography private key instead." + ), + DeprecationWarning, + stacklevel=2, + ) + + use_result = _lib.SSL_use_PrivateKey(self._ssl, pkey._pkey) + if not use_result: + self._context._raise_passphrase_exception() + + def set_ciphertext_mtu(self, mtu: int) -> None: + """ + For DTLS, set the maximum UDP payload size (*not* including IP/UDP + overhead). + + Note that you might have to set :data:`OP_NO_QUERY_MTU` to prevent + OpenSSL from spontaneously clearing this. + + :param mtu: An integer giving the maximum transmission unit. + + .. versionadded:: 21.1 + """ + _lib.SSL_set_mtu(self._ssl, mtu) + + def get_cleartext_mtu(self) -> int: + """ + For DTLS, get the maximum size of unencrypted data you can pass to + :meth:`write` without exceeding the MTU (as passed to + :meth:`set_ciphertext_mtu`). + + :return: The effective MTU as an integer. + + .. versionadded:: 21.1 + """ + + if not hasattr(_lib, "DTLS_get_data_mtu"): + raise NotImplementedError("requires OpenSSL 1.1.1 or better") + return _lib.DTLS_get_data_mtu(self._ssl) + + def set_tlsext_host_name(self, name: bytes) -> None: + """ + Set the value of the servername extension to send in the client hello. + + :param name: A byte string giving the name. + + .. versionadded:: 0.13 + """ + if not isinstance(name, bytes): + raise TypeError("name must be a byte string") + elif b"\0" in name: + raise TypeError("name must not contain NUL byte") + + # XXX I guess this can fail sometimes? + _lib.SSL_set_tlsext_host_name(self._ssl, name) + + def pending(self) -> int: + """ + Get the number of bytes that can be safely read from the SSL buffer + (**not** the underlying transport buffer). + + :return: The number of bytes available in the receive buffer. + """ + return _lib.SSL_pending(self._ssl) + + def send(self, buf: _Buffer, flags: int = 0) -> int: + """ + Send data on the connection. NOTE: If you get one of the WantRead, + WantWrite or WantX509Lookup exceptions on this, you have to call the + method again with the SAME buffer. + + :param buf: The string, buffer or memoryview to send + :param flags: (optional) Included for compatibility with the socket + API, the value is ignored + :return: The number of bytes written + """ + # Backward compatibility + buf = _text_to_bytes_and_warn("buf", buf) + + with _ffi.from_buffer(buf) as data: + # check len(buf) instead of len(data) for testability + if len(buf) > 2147483647: + raise ValueError( + "Cannot send more than 2**31-1 bytes at once." + ) + + result = _lib.SSL_write(self._ssl, data, len(data)) + self._raise_ssl_error(self._ssl, result) + + return result + + write = send + + def sendall(self, buf: _Buffer, flags: int = 0) -> int: + """ + Send "all" data on the connection. This calls send() repeatedly until + all data is sent. If an error occurs, it's impossible to tell how much + data has been sent. + + :param buf: The string, buffer or memoryview to send + :param flags: (optional) Included for compatibility with the socket + API, the value is ignored + :return: The number of bytes written + """ + buf = _text_to_bytes_and_warn("buf", buf) + + with _ffi.from_buffer(buf) as data: + left_to_send = len(buf) + total_sent = 0 + + while left_to_send: + # SSL_write's num arg is an int, + # so we cannot send more than 2**31-1 bytes at once. + result = _lib.SSL_write( + self._ssl, data + total_sent, min(left_to_send, 2147483647) + ) + self._raise_ssl_error(self._ssl, result) + total_sent += result + left_to_send -= result + + return total_sent + + def recv(self, bufsiz: int, flags: int | None = None) -> bytes: + """ + Receive data on the connection. + + :param bufsiz: The maximum number of bytes to read + :param flags: (optional) The only supported flag is ``MSG_PEEK``, + all other flags are ignored. + :return: The string read from the Connection + """ + buf = _no_zero_allocator("char[]", bufsiz) + if flags is not None and flags & socket.MSG_PEEK: + result = _lib.SSL_peek(self._ssl, buf, bufsiz) + else: + result = _lib.SSL_read(self._ssl, buf, bufsiz) + self._raise_ssl_error(self._ssl, result) + return _ffi.buffer(buf, result)[:] + + read = recv + + def recv_into( + self, + buffer: Any, # collections.abc.Buffer once we use Python 3.12+ + nbytes: int | None = None, + flags: int | None = None, + ) -> int: + """ + Receive data on the connection and copy it directly into the provided + buffer, rather than creating a new string. + + :param buffer: The buffer to copy into. + :param nbytes: (optional) The maximum number of bytes to read into the + buffer. If not present, defaults to the size of the buffer. If + larger than the size of the buffer, is reduced to the size of the + buffer. + :param flags: (optional) The only supported flag is ``MSG_PEEK``, + all other flags are ignored. + :return: The number of bytes read into the buffer. + """ + if nbytes is None: + nbytes = len(buffer) + else: + nbytes = min(nbytes, len(buffer)) + + # We need to create a temporary buffer. This is annoying, it would be + # better if we could pass memoryviews straight into the SSL_read call, + # but right now we can't. Revisit this if CFFI gets that ability. + buf = _no_zero_allocator("char[]", nbytes) + if flags is not None and flags & socket.MSG_PEEK: + result = _lib.SSL_peek(self._ssl, buf, nbytes) + else: + result = _lib.SSL_read(self._ssl, buf, nbytes) + self._raise_ssl_error(self._ssl, result) + + # This strange line is all to avoid a memory copy. The buffer protocol + # should allow us to assign a CFFI buffer to the LHS of this line, but + # on CPython 3.3+ that segfaults. As a workaround, we can temporarily + # wrap it in a memoryview. + buffer[:result] = memoryview(_ffi.buffer(buf, result)) + + return result + + def _handle_bio_errors(self, bio: Any, result: int) -> typing.NoReturn: + if _lib.BIO_should_retry(bio): + if _lib.BIO_should_read(bio): + raise WantReadError() + elif _lib.BIO_should_write(bio): + # TODO: This is untested. + raise WantWriteError() + elif _lib.BIO_should_io_special(bio): + # TODO: This is untested. I think io_special means the socket + # BIO has a not-yet connected socket. + raise ValueError("BIO_should_io_special") + else: + # TODO: This is untested. + raise ValueError("unknown bio failure") + else: + # TODO: This is untested. + _raise_current_error() + + def bio_read(self, bufsiz: int) -> bytes: + """ + If the Connection was created with a memory BIO, this method can be + used to read bytes from the write end of that memory BIO. Many + Connection methods will add bytes which must be read in this manner or + the buffer will eventually fill up and the Connection will be able to + take no further actions. + + :param bufsiz: The maximum number of bytes to read + :return: The string read. + """ + if self._from_ssl is None: + raise TypeError("Connection sock was not None") + + if not isinstance(bufsiz, int): + raise TypeError("bufsiz must be an integer") + + buf = _no_zero_allocator("char[]", bufsiz) + result = _lib.BIO_read(self._from_ssl, buf, bufsiz) + if result <= 0: + self._handle_bio_errors(self._from_ssl, result) + + return _ffi.buffer(buf, result)[:] + + def bio_write(self, buf: _Buffer) -> int: + """ + If the Connection was created with a memory BIO, this method can be + used to add bytes to the read end of that memory BIO. The Connection + can then read the bytes (for example, in response to a call to + :meth:`recv`). + + :param buf: The string to put into the memory BIO. + :return: The number of bytes written + """ + buf = _text_to_bytes_and_warn("buf", buf) + + if self._into_ssl is None: + raise TypeError("Connection sock was not None") + + with _ffi.from_buffer(buf) as data: + result = _lib.BIO_write(self._into_ssl, data, len(data)) + if result <= 0: + self._handle_bio_errors(self._into_ssl, result) + return result + + def renegotiate(self) -> bool: + """ + Renegotiate the session. + + :return: True if the renegotiation can be started, False otherwise + """ + if not self.renegotiate_pending(): + _openssl_assert(_lib.SSL_renegotiate(self._ssl) == 1) + return True + return False + + def do_handshake(self) -> None: + """ + Perform an SSL handshake (usually called after :meth:`renegotiate` or + one of :meth:`set_accept_state` or :meth:`set_connect_state`). This can + raise the same exceptions as :meth:`send` and :meth:`recv`. + + :return: None. + """ + result = _lib.SSL_do_handshake(self._ssl) + self._raise_ssl_error(self._ssl, result) + + def renegotiate_pending(self) -> bool: + """ + Check if there's a renegotiation in progress, it will return False once + a renegotiation is finished. + + :return: Whether there's a renegotiation in progress + """ + return _lib.SSL_renegotiate_pending(self._ssl) == 1 + + def total_renegotiations(self) -> int: + """ + Find out the total number of renegotiations. + + :return: The number of renegotiations. + """ + return _lib.SSL_total_renegotiations(self._ssl) + + def connect(self, addr: Any) -> None: + """ + Call the :meth:`connect` method of the underlying socket and set up SSL + on the socket, using the :class:`Context` object supplied to this + :class:`Connection` object at creation. + + :param addr: A remote address + :return: What the socket's connect method returns + """ + _lib.SSL_set_connect_state(self._ssl) + return self._socket.connect(addr) # type: ignore[return-value, union-attr] + + def connect_ex(self, addr: Any) -> int: + """ + Call the :meth:`connect_ex` method of the underlying socket and set up + SSL on the socket, using the Context object supplied to this Connection + object at creation. Note that if the :meth:`connect_ex` method of the + socket doesn't return 0, SSL won't be initialized. + + :param addr: A remove address + :return: What the socket's connect_ex method returns + """ + connect_ex = self._socket.connect_ex # type: ignore[union-attr] + self.set_connect_state() + return connect_ex(addr) + + def accept(self) -> tuple[Connection, Any]: + """ + Call the :meth:`accept` method of the underlying socket and set up SSL + on the returned socket, using the Context object supplied to this + :class:`Connection` object at creation. + + :return: A *(conn, addr)* pair where *conn* is the new + :class:`Connection` object created, and *address* is as returned by + the socket's :meth:`accept`. + """ + client, addr = self._socket.accept() # type: ignore[union-attr] + conn = Connection(self._context, client) + conn.set_accept_state() + return (conn, addr) + + def DTLSv1_listen(self) -> None: + """ + Call the OpenSSL function DTLSv1_listen on this connection. See the + OpenSSL manual for more details. + + :return: None + """ + # Possible future extension: return the BIO_ADDR in some form. + bio_addr = _lib.BIO_ADDR_new() + try: + result = _lib.DTLSv1_listen(self._ssl, bio_addr) + finally: + _lib.BIO_ADDR_free(bio_addr) + # DTLSv1_listen is weird. A zero return value means 'didn't find a + # ClientHello with valid cookie, but keep trying'. So basically + # WantReadError. But it doesn't work correctly with _raise_ssl_error. + # So we raise it manually instead. + if self._cookie_generate_helper is not None: + self._cookie_generate_helper.raise_if_problem() + if self._cookie_verify_helper is not None: + self._cookie_verify_helper.raise_if_problem() + if result == 0: + raise WantReadError() + if result < 0: + self._raise_ssl_error(self._ssl, result) + + def DTLSv1_get_timeout(self) -> int | None: + """ + Determine when the DTLS SSL object next needs to perform internal + processing due to the passage of time. + + When the returned number of seconds have passed, the + :meth:`DTLSv1_handle_timeout` method needs to be called. + + :return: The time left in seconds before the next timeout or `None` + if no timeout is currently active. + """ + ptv_sec = _ffi.new("time_t *") + ptv_usec = _ffi.new("long *") + if _lib.Cryptography_DTLSv1_get_timeout(self._ssl, ptv_sec, ptv_usec): + return ptv_sec[0] + (ptv_usec[0] / 1000000) + else: + return None + + def DTLSv1_handle_timeout(self) -> bool: + """ + Handles any timeout events which have become pending on a DTLS SSL + object. + + :return: `True` if there was a pending timeout, `False` otherwise. + """ + result = _lib.DTLSv1_handle_timeout(self._ssl) + if result < 0: + self._raise_ssl_error(self._ssl, result) + assert False, "unreachable" + else: + return bool(result) + + def bio_shutdown(self) -> None: + """ + If the Connection was created with a memory BIO, this method can be + used to indicate that *end of file* has been reached on the read end of + that memory BIO. + + :return: None + """ + if self._from_ssl is None: + raise TypeError("Connection sock was not None") + + _lib.BIO_set_mem_eof_return(self._into_ssl, 0) + + def shutdown(self) -> bool: + """ + Send the shutdown message to the Connection. + + :return: True if the shutdown completed successfully (i.e. both sides + have sent closure alerts), False otherwise (in which case you + call :meth:`recv` or :meth:`send` when the connection becomes + readable/writeable). + """ + result = _lib.SSL_shutdown(self._ssl) + if result < 0: + self._raise_ssl_error(self._ssl, result) + assert False, "unreachable" + elif result > 0: + return True + else: + return False + + def get_cipher_list(self) -> list[str]: + """ + Retrieve the list of ciphers used by the Connection object. + + :return: A list of native cipher strings. + """ + ciphers = [] + for i in count(): + result = _lib.SSL_get_cipher_list(self._ssl, i) + if result == _ffi.NULL: + break + ciphers.append(_ffi.string(result).decode("utf-8")) + return ciphers + + @typing.overload + def get_client_ca_list( + self, *, as_cryptography: typing.Literal[True] + ) -> list[x509.Name]: + pass + + @typing.overload + def get_client_ca_list( + self, *, as_cryptography: typing.Literal[False] = False + ) -> list[X509Name]: + pass + + def get_client_ca_list( + self, + *, + as_cryptography: typing.Literal[True] | typing.Literal[False] = False, + ) -> list[X509Name] | list[x509.Name]: + """ + Get CAs whose certificates are suggested for client authentication. + + :param bool as_cryptography: Controls whether a list of + ``cryptography.x509.Name`` or ``OpenSSL.crypto.X509Name`` + objects should be returned. + + :return: If this is a server connection, the list of certificate + authorities that will be sent or has been sent to the client, as + controlled by this :class:`Connection`'s :class:`Context`. + + If this is a client connection, the list will be empty until the + connection with the server is established. + + .. versionadded:: 0.10 + """ + ca_names = _lib.SSL_get_client_CA_list(self._ssl) + if ca_names == _ffi.NULL: + # TODO: This is untested. + return [] + + if as_cryptography: + names = [] + for i in range(_lib.sk_X509_NAME_num(ca_names)): + name = _lib.sk_X509_NAME_value(ca_names, i) + result_buffer = _ffi.new("unsigned char**") + encode_result = _lib.i2d_X509_NAME(name, result_buffer) + _openssl_assert(encode_result >= 0) + der = _ffi.buffer(result_buffer[0], encode_result)[:] + _lib.OPENSSL_free(result_buffer[0]) + + names.append(x509.Name.from_bytes(der)) + return names + + result = [] + for i in range(_lib.sk_X509_NAME_num(ca_names)): + name = _lib.sk_X509_NAME_value(ca_names, i) + copy = _lib.X509_NAME_dup(name) + _openssl_assert(copy != _ffi.NULL) + + # Bypass X509Name.__new__, which warns that X509Name is + # deprecated -- this method is not itself deprecated. + pyname = object.__new__(X509Name) + pyname._name = _ffi.gc(copy, _lib.X509_NAME_free) + result.append(pyname) + return result + + def makefile(self, *args: Any, **kwargs: Any) -> typing.NoReturn: + """ + The makefile() method is not implemented, since there is no dup + semantics for SSL connections + + :raise: NotImplementedError + """ + raise NotImplementedError( + "Cannot make file object of OpenSSL.SSL.Connection" + ) + + def get_app_data(self) -> Any: + """ + Retrieve application data as set by :meth:`set_app_data`. + + :return: The application data + """ + return self._app_data + + def set_app_data(self, data: Any) -> None: + """ + Set application data + + :param data: The application data + :return: None + """ + self._app_data = data + + def get_shutdown(self) -> int: + """ + Get the shutdown state of the Connection. + + :return: The shutdown state, a bitvector of SENT_SHUTDOWN, + RECEIVED_SHUTDOWN. + """ + return _lib.SSL_get_shutdown(self._ssl) + + def set_shutdown(self, state: int) -> None: + """ + Set the shutdown state of the Connection. + + :param state: bitvector of SENT_SHUTDOWN, RECEIVED_SHUTDOWN. + :return: None + """ + if not isinstance(state, int): + raise TypeError("state must be an integer") + + _lib.SSL_set_shutdown(self._ssl, state) + + def get_state_string(self) -> bytes: + """ + Retrieve a verbose string detailing the state of the Connection. + + :return: A string representing the state + """ + return _ffi.string(_lib.SSL_state_string_long(self._ssl)) + + def server_random(self) -> bytes | None: + """ + Retrieve the random value used with the server hello message. + + :return: A string representing the state + """ + session = _lib.SSL_get_session(self._ssl) + if session == _ffi.NULL: + return None + length = _lib.SSL_get_server_random(self._ssl, _ffi.NULL, 0) + _openssl_assert(length > 0) + outp = _no_zero_allocator("unsigned char[]", length) + _lib.SSL_get_server_random(self._ssl, outp, length) + return _ffi.buffer(outp, length)[:] + + def client_random(self) -> bytes | None: + """ + Retrieve the random value used with the client hello message. + + :return: A string representing the state + """ + session = _lib.SSL_get_session(self._ssl) + if session == _ffi.NULL: + return None + + length = _lib.SSL_get_client_random(self._ssl, _ffi.NULL, 0) + _openssl_assert(length > 0) + outp = _no_zero_allocator("unsigned char[]", length) + _lib.SSL_get_client_random(self._ssl, outp, length) + return _ffi.buffer(outp, length)[:] + + def master_key(self) -> bytes | None: + """ + Retrieve the value of the master key for this session. + + :return: A string representing the state + """ + session = _lib.SSL_get_session(self._ssl) + if session == _ffi.NULL: + return None + + length = _lib.SSL_SESSION_get_master_key(session, _ffi.NULL, 0) + _openssl_assert(length > 0) + outp = _no_zero_allocator("unsigned char[]", length) + _lib.SSL_SESSION_get_master_key(session, outp, length) + return _ffi.buffer(outp, length)[:] + + def export_keying_material( + self, label: bytes, olen: int, context: bytes | None = None + ) -> bytes: + """ + Obtain keying material for application use. + + :param: label - a disambiguating label string as described in RFC 5705 + :param: olen - the length of the exported key material in bytes + :param: context - a per-association context value + :return: the exported key material bytes or None + """ + outp = _no_zero_allocator("unsigned char[]", olen) + context_buf = _ffi.NULL + context_len = 0 + use_context = 0 + if context is not None: + context_buf = context + context_len = len(context) + use_context = 1 + success = _lib.SSL_export_keying_material( + self._ssl, + outp, + olen, + label, + len(label), + context_buf, + context_len, + use_context, + ) + _openssl_assert(success == 1) + return _ffi.buffer(outp, olen)[:] + + def sock_shutdown(self, *args: Any, **kwargs: Any) -> None: + """ + Call the :meth:`shutdown` method of the underlying socket. + See :manpage:`shutdown(2)`. + + :return: What the socket's shutdown() method returns + """ + return self._socket.shutdown(*args, **kwargs) # type: ignore[return-value, union-attr] + + @typing.overload + def get_certificate( + self, *, as_cryptography: typing.Literal[True] + ) -> x509.Certificate | None: + pass + + @typing.overload + def get_certificate( + self, *, as_cryptography: typing.Literal[False] = False + ) -> X509 | None: + pass + + def get_certificate( + self, + *, + as_cryptography: typing.Literal[True] | typing.Literal[False] = False, + ) -> X509 | x509.Certificate | None: + """ + Retrieve the local certificate (if any) + + :param bool as_cryptography: Controls whether a + ``cryptography.x509.Certificate`` or an ``OpenSSL.crypto.X509`` + object should be returned. + + :return: The local certificate + """ + cert = _lib.SSL_get_certificate(self._ssl) + if cert != _ffi.NULL: + _lib.X509_up_ref(cert) + pycert = X509._from_raw_x509_ptr(cert) + if as_cryptography: + return pycert.to_cryptography() + return pycert + return None + + @typing.overload + def get_peer_certificate( + self, *, as_cryptography: typing.Literal[True] + ) -> x509.Certificate | None: + pass + + @typing.overload + def get_peer_certificate( + self, *, as_cryptography: typing.Literal[False] = False + ) -> X509 | None: + pass + + def get_peer_certificate( + self, + *, + as_cryptography: typing.Literal[True] | typing.Literal[False] = False, + ) -> X509 | x509.Certificate | None: + """ + Retrieve the other side's certificate (if any) + + :param bool as_cryptography: Controls whether a + ``cryptography.x509.Certificate`` or an ``OpenSSL.crypto.X509`` + object should be returned. + + :return: The peer's certificate + """ + cert = _lib.SSL_get_peer_certificate(self._ssl) + if cert != _ffi.NULL: + pycert = X509._from_raw_x509_ptr(cert) + if as_cryptography: + return pycert.to_cryptography() + return pycert + return None + + @staticmethod + def _cert_stack_to_list(cert_stack: Any) -> list[X509]: + """ + Internal helper to convert a STACK_OF(X509) to a list of X509 + instances. + """ + result = [] + for i in range(_lib.sk_X509_num(cert_stack)): + cert = _lib.sk_X509_value(cert_stack, i) + _openssl_assert(cert != _ffi.NULL) + res = _lib.X509_up_ref(cert) + _openssl_assert(res >= 1) + pycert = X509._from_raw_x509_ptr(cert) + result.append(pycert) + return result + + @staticmethod + def _cert_stack_to_cryptography_list( + cert_stack: Any, + ) -> list[x509.Certificate]: + """ + Internal helper to convert a STACK_OF(X509) to a list of X509 + instances. + """ + result = [] + for i in range(_lib.sk_X509_num(cert_stack)): + cert = _lib.sk_X509_value(cert_stack, i) + _openssl_assert(cert != _ffi.NULL) + res = _lib.X509_up_ref(cert) + _openssl_assert(res >= 1) + pycert = X509._from_raw_x509_ptr(cert) + result.append(pycert.to_cryptography()) + return result + + @typing.overload + def get_peer_cert_chain( + self, *, as_cryptography: typing.Literal[True] + ) -> list[x509.Certificate] | None: + pass + + @typing.overload + def get_peer_cert_chain( + self, *, as_cryptography: typing.Literal[False] = False + ) -> list[X509] | None: + pass + + def get_peer_cert_chain( + self, + *, + as_cryptography: typing.Literal[True] | typing.Literal[False] = False, + ) -> list[X509] | list[x509.Certificate] | None: + """ + Retrieve the other side's certificate (if any) + + :param bool as_cryptography: Controls whether a list of + ``cryptography.x509.Certificate`` or ``OpenSSL.crypto.X509`` + object should be returned. + + :return: A list of X509 instances giving the peer's certificate chain, + or None if it does not have one. + """ + cert_stack = _lib.SSL_get_peer_cert_chain(self._ssl) + if cert_stack == _ffi.NULL: + return None + + if as_cryptography: + return self._cert_stack_to_cryptography_list(cert_stack) + return self._cert_stack_to_list(cert_stack) + + @typing.overload + def get_verified_chain( + self, *, as_cryptography: typing.Literal[True] + ) -> list[x509.Certificate] | None: + pass + + @typing.overload + def get_verified_chain( + self, *, as_cryptography: typing.Literal[False] = False + ) -> list[X509] | None: + pass + + def get_verified_chain( + self, + *, + as_cryptography: typing.Literal[True] | typing.Literal[False] = False, + ) -> list[X509] | list[x509.Certificate] | None: + """ + Retrieve the verified certificate chain of the peer including the + peer's end entity certificate. It must be called after a session has + been successfully established. If peer verification was not successful + the chain may be incomplete, invalid, or None. + + :param bool as_cryptography: Controls whether a list of + ``cryptography.x509.Certificate`` or ``OpenSSL.crypto.X509`` + object should be returned. + + :return: A list of X509 instances giving the peer's verified + certificate chain, or None if it does not have one. + + .. versionadded:: 20.0 + """ + # OpenSSL 1.1+ + cert_stack = _lib.SSL_get0_verified_chain(self._ssl) + if cert_stack == _ffi.NULL: + return None + + if as_cryptography: + return self._cert_stack_to_cryptography_list(cert_stack) + return self._cert_stack_to_list(cert_stack) + + def want_read(self) -> bool: + """ + Checks if more data has to be read from the transport layer to complete + an operation. + + :return: True iff more data has to be read + """ + return _lib.SSL_want_read(self._ssl) + + def want_write(self) -> bool: + """ + Checks if there is data to write to the transport layer to complete an + operation. + + :return: True iff there is data to write + """ + return _lib.SSL_want_write(self._ssl) + + def set_accept_state(self) -> None: + """ + Set the connection to work in server mode. The handshake will be + handled automatically by read/write. + + :return: None + """ + _lib.SSL_set_accept_state(self._ssl) + + def set_connect_state(self) -> None: + """ + Set the connection to work in client mode. The handshake will be + handled automatically by read/write. + + :return: None + """ + _lib.SSL_set_connect_state(self._ssl) + + def get_session(self) -> Session | None: + """ + Returns the Session currently used. + + :return: An instance of :class:`OpenSSL.SSL.Session` or + :obj:`None` if no session exists. + + .. versionadded:: 0.14 + """ + session = _lib.SSL_get1_session(self._ssl) + if session == _ffi.NULL: + return None + + pysession = Session.__new__(Session) + pysession._session = _ffi.gc(session, _lib.SSL_SESSION_free) + pysession._context = self._context + return pysession + + def set_session(self, session: Session) -> None: + """ + Set the session to be used when the TLS/SSL connection is established. + + The session must have been obtained, via :meth:`get_session`, from a + :class:`Connection` that was using the same :class:`Context` as this + one. OpenSSL requires (but does not verify) that sessions only be + re-used with a compatible ``SSL_CTX``, so this is enforced here. + + :param session: A Session instance representing the session to use. + :returns: None + + .. versionadded:: 0.14 + """ + if not isinstance(session, Session): + raise TypeError("session must be a Session instance") + + if session._context is not self._context: + raise ValueError( + "session must have been created by a Connection using the " + "same Context as this one" + ) + + result = _lib.SSL_set_session(self._ssl, session._session) + _openssl_assert(result == 1) + + def _get_finished_message( + self, function: Callable[[Any, Any, int], int] + ) -> bytes | None: + """ + Helper to implement :meth:`get_finished` and + :meth:`get_peer_finished`. + + :param function: Either :data:`SSL_get_finished`: or + :data:`SSL_get_peer_finished`. + + :return: :data:`None` if the desired message has not yet been + received, otherwise the contents of the message. + """ + # The OpenSSL documentation says nothing about what might happen if the + # count argument given is zero. Specifically, it doesn't say whether + # the output buffer may be NULL in that case or not. Inspection of the + # implementation reveals that it calls memcpy() unconditionally. + # Section 7.1.4, paragraph 1 of the C standard suggests that + # memcpy(NULL, source, 0) is not guaranteed to produce defined (let + # alone desirable) behavior (though it probably does on just about + # every implementation...) + # + # Allocate a tiny buffer to pass in (instead of just passing NULL as + # one might expect) for the initial call so as to be safe against this + # potentially undefined behavior. + empty = _ffi.new("char[]", 0) + size = function(self._ssl, empty, 0) + if size == 0: + # No Finished message so far. + return None + + buf = _no_zero_allocator("char[]", size) + function(self._ssl, buf, size) + return _ffi.buffer(buf, size)[:] + + def get_finished(self) -> bytes | None: + """ + Obtain the latest TLS Finished message that we sent. + + :return: The contents of the message or :obj:`None` if the TLS + handshake has not yet completed. + + .. versionadded:: 0.15 + """ + return self._get_finished_message(_lib.SSL_get_finished) + + def get_peer_finished(self) -> bytes | None: + """ + Obtain the latest TLS Finished message that we received from the peer. + + :return: The contents of the message or :obj:`None` if the TLS + handshake has not yet completed. + + .. versionadded:: 0.15 + """ + return self._get_finished_message(_lib.SSL_get_peer_finished) + + def get_cipher_name(self) -> str | None: + """ + Obtain the name of the currently used cipher. + + :returns: The name of the currently used cipher or :obj:`None` + if no connection has been established. + + .. versionadded:: 0.15 + """ + cipher = _lib.SSL_get_current_cipher(self._ssl) + if cipher == _ffi.NULL: + return None + else: + name = _ffi.string(_lib.SSL_CIPHER_get_name(cipher)) + return name.decode("utf-8") + + def get_cipher_bits(self) -> int | None: + """ + Obtain the number of secret bits of the currently used cipher. + + :returns: The number of secret bits of the currently used cipher + or :obj:`None` if no connection has been established. + + .. versionadded:: 0.15 + """ + cipher = _lib.SSL_get_current_cipher(self._ssl) + if cipher == _ffi.NULL: + return None + else: + return _lib.SSL_CIPHER_get_bits(cipher, _ffi.NULL) + + def get_cipher_version(self) -> str | None: + """ + Obtain the protocol version of the currently used cipher. + + :returns: The protocol name of the currently used cipher + or :obj:`None` if no connection has been established. + + .. versionadded:: 0.15 + """ + cipher = _lib.SSL_get_current_cipher(self._ssl) + if cipher == _ffi.NULL: + return None + else: + version = _ffi.string(_lib.SSL_CIPHER_get_version(cipher)) + return version.decode("utf-8") + + def get_protocol_version_name(self) -> str: + """ + Retrieve the protocol version of the current connection. + + :returns: The TLS version of the current connection, for example + the value for TLS 1.2 would be ``TLSv1.2``or ``Unknown`` + for connections that were not successfully established. + """ + version = _ffi.string(_lib.SSL_get_version(self._ssl)) + return version.decode("utf-8") + + def get_protocol_version(self) -> int: + """ + Retrieve the SSL or TLS protocol version of the current connection. + + :returns: The TLS version of the current connection. For example, + it will return ``0x769`` for connections made over TLS version 1. + """ + version = _lib.SSL_version(self._ssl) + return version + + def set_alpn_protos(self, protos: list[bytes]) -> None: + """ + Specify the client's ALPN protocol list. + + These protocols are offered to the server during protocol negotiation. + + :param protos: A list of the protocols to be offered to the server. + This list should be a Python list of bytestrings representing the + protocols to offer, e.g. ``[b'http/1.1', b'spdy/2']``. + """ + # Different versions of OpenSSL are inconsistent about how they handle + # empty proto lists (see #1043), so we avoid the problem entirely by + # rejecting them ourselves. + if not protos: + raise ValueError("at least one protocol must be specified") + + # Take the list of protocols and join them together, prefixing them + # with their lengths. + protostr = b"".join( + chain.from_iterable((bytes((len(p),)), p) for p in protos) + ) + + # Build a C string from the list. We don't need to save this off + # because OpenSSL immediately copies the data out. + input_str = _ffi.new("unsigned char[]", protostr) + + # https://www.openssl.org/docs/man1.1.0/man3/SSL_CTX_set_alpn_protos.html: + # SSL_CTX_set_alpn_protos() and SSL_set_alpn_protos() + # return 0 on success, and non-0 on failure. + # WARNING: these functions reverse the return value convention. + _openssl_assert( + _lib.SSL_set_alpn_protos(self._ssl, input_str, len(protostr)) == 0 + ) + + def get_alpn_proto_negotiated(self) -> bytes: + """ + Get the protocol that was negotiated by ALPN. + + :returns: A bytestring of the protocol name. If no protocol has been + negotiated yet, returns an empty bytestring. + """ + data = _ffi.new("unsigned char **") + data_len = _ffi.new("unsigned int *") + + _lib.SSL_get0_alpn_selected(self._ssl, data, data_len) + + if not data_len: + return b"" + + return _ffi.buffer(data[0], data_len[0])[:] + + def get_selected_srtp_profile(self) -> bytes: + """ + Get the SRTP protocol which was negotiated. + + :returns: A bytestring of the SRTP profile name. If no profile has been + negotiated yet, returns an empty bytestring. + """ + profile = _lib.SSL_get_selected_srtp_profile(self._ssl) + if not profile: + return b"" + + return _ffi.string(profile.name) + + @_requires_ssl_get0_group_name + def get_group_name(self) -> str | None: + """ + Get the name of the negotiated group for the key exchange. + + :return: A string giving the group name or :data:`None`. + """ + # Do not remove this guard. + # SSL_get0_group_name crashes with a segfault if called without + # an established connection (should return NULL but doesn't). + session = _lib.SSL_get_session(self._ssl) + if session == _ffi.NULL: + return None + + group_name = _lib.SSL_get0_group_name(self._ssl) + if group_name == _ffi.NULL: + return None + + return _ffi.string(group_name).decode("utf-8") + + def request_ocsp(self) -> None: + """ + Called to request that the server sends stapled OCSP data, if + available. If this is not called on the client side then the server + will not send OCSP data. Should be used in conjunction with + :meth:`Context.set_ocsp_client_callback`. + """ + rc = _lib.SSL_set_tlsext_status_type( + self._ssl, _lib.TLSEXT_STATUSTYPE_ocsp + ) + _openssl_assert(rc == 1) + + def set_info_callback( + self, callback: Callable[[Connection, int, int], None] + ) -> None: + """ + Set the information callback to *callback*. This function will be + called from time to time during SSL handshakes. + + :param callback: The Python callback to use. This should take three + arguments: a Connection object and two integers. The first integer + specifies where in the SSL handshake the function was called, and + the other the return code from a (possibly failed) internal + function call. + :return: None + """ + + @wraps(callback) + def wrapper(ssl, where, return_code): # type: ignore[no-untyped-def] + callback(Connection._reverse_mapping[ssl], where, return_code) + + self._info_callback = _ffi.callback( + "void (*)(const SSL *, int, int)", wrapper + ) + _lib.SSL_set_info_callback(self._ssl, self._info_callback) diff --git a/src/OpenSSL/__init__.py b/src/OpenSSL/__init__.py new file mode 100644 index 000000000..7b077cf70 --- /dev/null +++ b/src/OpenSSL/__init__.py @@ -0,0 +1,31 @@ +# Copyright (C) AB Strakt +# See LICENSE for details. + +""" +pyOpenSSL - A simple wrapper around the OpenSSL library +""" + +from OpenSSL import SSL, crypto +from OpenSSL.version import ( + __author__, + __copyright__, + __email__, + __license__, + __summary__, + __title__, + __uri__, + __version__, +) + +__all__ = [ + "SSL", + "__author__", + "__copyright__", + "__email__", + "__license__", + "__summary__", + "__title__", + "__uri__", + "__version__", + "crypto", +] diff --git a/src/OpenSSL/_util.py b/src/OpenSSL/_util.py new file mode 100644 index 000000000..3bed35977 --- /dev/null +++ b/src/OpenSSL/_util.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import os +import sys +import warnings +from typing import Any, Callable, NoReturn, Union + +from cryptography.hazmat.bindings.openssl.binding import Binding + +StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] + +binding = Binding() +ffi = binding.ffi +lib: Any = binding.lib + + +# This is a special CFFI allocator that does not bother to zero its memory +# after allocation. This has vastly better performance on large allocations and +# so should be used whenever we don't need the memory zeroed out. +no_zero_allocator = ffi.new_allocator(should_clear_after_alloc=False) + + +def text(charp: Any) -> str: + """ + Get a native string type representing of the given CFFI ``char*`` object. + + :param charp: A C-style string represented using CFFI. + + :return: :class:`str` + """ + if not charp: + return "" + return ffi.string(charp).decode("utf-8") + + +def exception_from_error_queue(exception_type: type[Exception]) -> NoReturn: + """ + Convert an OpenSSL library failure into a Python exception. + + When a call to the native OpenSSL library fails, this is usually signalled + by the return value, and an error code is stored in an error queue + associated with the current thread. The err library provides functions to + obtain these error codes and textual error messages. + """ + errors = [] + + while True: + error = lib.ERR_get_error() + if error == 0: + break + errors.append( + ( + text(lib.ERR_lib_error_string(error)), + text(lib.ERR_func_error_string(error)), + text(lib.ERR_reason_error_string(error)), + ) + ) + + raise exception_type(errors) + + +def make_assert(error: type[Exception]) -> Callable[[bool], Any]: + """ + Create an assert function that uses :func:`exception_from_error_queue` to + raise an exception wrapped by *error*. + """ + + def openssl_assert(ok: bool) -> None: + """ + If *ok* is not True, retrieve the error from OpenSSL and raise it. + """ + if ok is not True: + exception_from_error_queue(error) + + return openssl_assert + + +def path_bytes(s: StrOrBytesPath) -> bytes: + """ + Convert a Python path to a :py:class:`bytes` for the path which can be + passed into an OpenSSL API accepting a filename. + + :param s: A path (valid for os.fspath). + + :return: An instance of :py:class:`bytes`. + """ + b = os.fspath(s) + + if isinstance(b, str): + return b.encode(sys.getfilesystemencoding()) + else: + return b + + +def byte_string(s: str) -> bytes: + return s.encode("charmap") + + +# A marker object to observe whether some optional arguments are passed any +# value or not. +UNSPECIFIED = object() + +_TEXT_WARNING = "str for {0} is no longer accepted, use bytes" + + +def text_to_bytes_and_warn(label: str, obj: Any) -> Any: + """ + If ``obj`` is text, emit a warning that it should be bytes instead and try + to convert it to bytes automatically. + + :param str label: The name of the parameter from which ``obj`` was taken + (so a developer can easily find the source of the problem and correct + it). + + :return: If ``obj`` is the text string type, a ``bytes`` object giving the + UTF-8 encoding of that text is returned. Otherwise, ``obj`` itself is + returned. + """ + if isinstance(obj, str): + warnings.warn( + _TEXT_WARNING.format(label), + category=DeprecationWarning, + stacklevel=3, + ) + return obj.encode("utf-8") + return obj diff --git a/src/OpenSSL/crypto.py b/src/OpenSSL/crypto.py new file mode 100644 index 000000000..314b577fe --- /dev/null +++ b/src/OpenSSL/crypto.py @@ -0,0 +1,1950 @@ +from __future__ import annotations + +import calendar +import datetime +import functools +import sys +import typing +from base64 import b16encode +from collections.abc import Sequence +from functools import partial +from typing import ( + Any, + Callable, + Union, +) + +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated + +from cryptography import utils, x509 +from cryptography.hazmat.primitives.asymmetric import ( + dsa, + ec, + ed448, + ed25519, + rsa, +) + +from OpenSSL._util import StrOrBytesPath +from OpenSSL._util import ( + byte_string as _byte_string, +) +from OpenSSL._util import ( + exception_from_error_queue as _exception_from_error_queue, +) +from OpenSSL._util import ( + ffi as _ffi, +) +from OpenSSL._util import ( + lib as _lib, +) +from OpenSSL._util import ( + make_assert as _make_assert, +) +from OpenSSL._util import ( + path_bytes as _path_bytes, +) + +__all__ = [ + "FILETYPE_ASN1", + "FILETYPE_PEM", + "FILETYPE_TEXT", + "TYPE_DSA", + "TYPE_RSA", + "X509", + "Error", + "PKey", + "X509Name", + "X509Store", + "X509StoreContext", + "X509StoreContextError", + "X509StoreFlags", + "dump_certificate", + "dump_privatekey", + "dump_publickey", + "get_elliptic_curve", + "get_elliptic_curves", + "load_certificate", + "load_privatekey", + "load_publickey", +] + + +_PrivateKey = Union[ + dsa.DSAPrivateKey, + ec.EllipticCurvePrivateKey, + ed25519.Ed25519PrivateKey, + ed448.Ed448PrivateKey, + rsa.RSAPrivateKey, +] +_PublicKey = Union[ + dsa.DSAPublicKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey, + ed448.Ed448PublicKey, + rsa.RSAPublicKey, +] +_Key = Union[_PrivateKey, _PublicKey] +PassphraseCallableT = Union[bytes, Callable[..., bytes]] + + +FILETYPE_PEM: int = _lib.SSL_FILETYPE_PEM +FILETYPE_ASN1: int = _lib.SSL_FILETYPE_ASN1 + +# TODO This was an API mistake. OpenSSL has no such constant. +FILETYPE_TEXT = 2**16 - 1 + +TYPE_RSA: int = _lib.EVP_PKEY_RSA +TYPE_DSA: int = _lib.EVP_PKEY_DSA +TYPE_DH: int = _lib.EVP_PKEY_DH +TYPE_EC: int = _lib.EVP_PKEY_EC + + +class Error(Exception): + """ + An error occurred in an `OpenSSL.crypto` API. + """ + + +_raise_current_error = partial(_exception_from_error_queue, Error) +_openssl_assert = _make_assert(Error) + + +def _new_mem_buf(buffer: bytes | None = None) -> Any: + """ + Allocate a new OpenSSL memory BIO. + + Arrange for the garbage collector to clean it up automatically. + + :param buffer: None or some bytes to use to put into the BIO so that they + can be read out. + """ + if buffer is None: + bio = _lib.BIO_new(_lib.BIO_s_mem()) + free = _lib.BIO_free + else: + data = _ffi.new("char[]", buffer) + bio = _lib.BIO_new_mem_buf(data, len(buffer)) + + # Keep the memory alive as long as the bio is alive! + def free(bio: Any, ref: Any = data) -> Any: + return _lib.BIO_free(bio) + + _openssl_assert(bio != _ffi.NULL) + + bio = _ffi.gc(bio, free) + return bio + + +def _bio_to_string(bio: Any) -> bytes: + """ + Copy the contents of an OpenSSL BIO object into a Python byte string. + """ + result_buffer = _ffi.new("char**") + buffer_length = _lib.BIO_get_mem_data(bio, result_buffer) + return _ffi.buffer(result_buffer[0], buffer_length)[:] + + +def _set_asn1_time(boundary: Any, when: bytes) -> None: + """ + The the time value of an ASN1 time object. + + @param boundary: An ASN1_TIME pointer (or an object safely + castable to that type) which will have its value set. + @param when: A string representation of the desired time value. + + @raise TypeError: If C{when} is not a L{bytes} string. + @raise ValueError: If C{when} does not represent a time in the required + format. + @raise RuntimeError: If the time value cannot be set for some other + (unspecified) reason. + """ + if not isinstance(when, bytes): + raise TypeError("when must be a byte string") + # ASN1_TIME_set_string validates the string without writing anything + # when the destination is NULL. + _openssl_assert(boundary != _ffi.NULL) + + set_result = _lib.ASN1_TIME_set_string(boundary, when) + if set_result == 0: + raise ValueError("Invalid string") + + +def _new_asn1_time(when: bytes) -> Any: + """ + Behaves like _set_asn1_time but returns a new ASN1_TIME object. + + @param when: A string representation of the desired time value. + + @raise TypeError: If C{when} is not a L{bytes} string. + @raise ValueError: If C{when} does not represent a time in the required + format. + @raise RuntimeError: If the time value cannot be set for some other + (unspecified) reason. + """ + ret = _lib.ASN1_TIME_new() + _openssl_assert(ret != _ffi.NULL) + ret = _ffi.gc(ret, _lib.ASN1_TIME_free) + _set_asn1_time(ret, when) + return ret + + +def _get_asn1_time(timestamp: Any) -> bytes | None: + """ + Retrieve the time value of an ASN1 time object. + + @param timestamp: An ASN1_GENERALIZEDTIME* (or an object safely castable to + that type) from which the time value will be retrieved. + + @return: The time value from C{timestamp} as a L{bytes} string in a certain + format. Or C{None} if the object contains no time value. + """ + string_timestamp = _ffi.cast("ASN1_STRING*", timestamp) + if _lib.ASN1_STRING_length(string_timestamp) == 0: + return None + elif ( + _lib.ASN1_STRING_type(string_timestamp) == _lib.V_ASN1_GENERALIZEDTIME + ): + return _ffi.string(_lib.ASN1_STRING_get0_data(string_timestamp)) + else: + generalized_timestamp = _ffi.new("ASN1_GENERALIZEDTIME**") + _lib.ASN1_TIME_to_generalizedtime(timestamp, generalized_timestamp) + _openssl_assert(generalized_timestamp[0] != _ffi.NULL) + + string_timestamp = _ffi.cast("ASN1_STRING*", generalized_timestamp[0]) + string_data = _lib.ASN1_STRING_get0_data(string_timestamp) + string_result = _ffi.string(string_data) + _lib.ASN1_GENERALIZEDTIME_free(generalized_timestamp[0]) + return string_result + + +class _X509NameInvalidator: + def __init__(self) -> None: + self._names: list[X509Name] = [] + + def add(self, name: X509Name) -> None: + self._names.append(name) + + def clear(self) -> None: + for name in self._names: + # Breaks the object, but also prevents UAF! + del name._name + + +class PKey: + """ + A class representing an DSA or RSA public key or key pair. + """ + + _only_public = False + _initialized = True + + def __init__(self) -> None: + pkey = _lib.EVP_PKEY_new() + self._pkey = _ffi.gc(pkey, _lib.EVP_PKEY_free) + self._initialized = False + + def to_cryptography_key(self) -> _Key: + """ + Export as a ``cryptography`` key. + + :rtype: One of ``cryptography``'s `key interfaces`_. + + .. _key interfaces: https://cryptography.io/en/latest/hazmat/\ + primitives/asymmetric/rsa/#key-interfaces + + .. versionadded:: 16.1.0 + """ + from cryptography.hazmat.primitives.serialization import ( + load_der_private_key, + load_der_public_key, + ) + + if self._only_public: + der = dump_publickey(FILETYPE_ASN1, self) + return typing.cast(_Key, load_der_public_key(der)) + else: + der = _dump_privatekey_internal(FILETYPE_ASN1, self) + return typing.cast(_Key, load_der_private_key(der, password=None)) + + @classmethod + def from_cryptography_key(cls, crypto_key: _Key) -> PKey: + """ + Construct based on a ``cryptography`` *crypto_key*. + + :param crypto_key: A ``cryptography`` key. + :type crypto_key: One of ``cryptography``'s `key interfaces`_. + + :rtype: PKey + + .. versionadded:: 16.1.0 + """ + if not isinstance( + crypto_key, + ( + dsa.DSAPrivateKey, + dsa.DSAPublicKey, + ec.EllipticCurvePrivateKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PrivateKey, + ed25519.Ed25519PublicKey, + ed448.Ed448PrivateKey, + ed448.Ed448PublicKey, + rsa.RSAPrivateKey, + rsa.RSAPublicKey, + ), + ): + raise TypeError("Unsupported key type") + + from cryptography.hazmat.primitives.serialization import ( + Encoding, + NoEncryption, + PrivateFormat, + PublicFormat, + ) + + if isinstance( + crypto_key, + ( + dsa.DSAPublicKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey, + ed448.Ed448PublicKey, + rsa.RSAPublicKey, + ), + ): + return load_publickey( + FILETYPE_ASN1, + crypto_key.public_bytes( + Encoding.DER, PublicFormat.SubjectPublicKeyInfo + ), + ) + else: + der = crypto_key.private_bytes( + Encoding.DER, PrivateFormat.PKCS8, NoEncryption() + ) + return load_privatekey(FILETYPE_ASN1, der) + + @deprecated( + "PKey.generate_key is deprecated. You should use the key " + "generation APIs in cryptography instead." + ) + def generate_key(self, type: int, bits: int) -> None: + """ + Generate a key pair of the given type, with the given number of bits. + + This generates a key "into" the this object. + + :param type: The key type. + :type type: :py:data:`TYPE_RSA` or :py:data:`TYPE_DSA` + :param bits: The number of bits. + :type bits: :py:data:`int` ``>= 0`` + :raises TypeError: If :py:data:`type` or :py:data:`bits` isn't + of the appropriate type. + :raises ValueError: If the number of bits isn't an integer of + the appropriate size. + :return: ``None`` + """ + if not isinstance(type, int): + raise TypeError("type must be an integer") + + if not isinstance(bits, int): + raise TypeError("bits must be an integer") + + if type == TYPE_RSA: + if bits <= 0: + raise ValueError("Invalid number of bits") + + # TODO Check error return + exponent = _lib.BN_new() + exponent = _ffi.gc(exponent, _lib.BN_free) + _lib.BN_set_word(exponent, _lib.RSA_F4) + + rsa = _lib.RSA_new() + + result = _lib.RSA_generate_key_ex(rsa, bits, exponent, _ffi.NULL) + _openssl_assert(result == 1) + + result = _lib.EVP_PKEY_assign_RSA(self._pkey, rsa) + _openssl_assert(result == 1) + + elif type == TYPE_DSA: + dsa = _lib.DSA_new() + _openssl_assert(dsa != _ffi.NULL) + + dsa = _ffi.gc(dsa, _lib.DSA_free) + res = _lib.DSA_generate_parameters_ex( + dsa, bits, _ffi.NULL, 0, _ffi.NULL, _ffi.NULL, _ffi.NULL + ) + _openssl_assert(res == 1) + + _openssl_assert(_lib.DSA_generate_key(dsa) == 1) + _openssl_assert(_lib.EVP_PKEY_set1_DSA(self._pkey, dsa) == 1) + else: + raise Error("No such key type") + + self._initialized = True + + @deprecated( + "PKey.check is deprecated. You should use the APIs in " + "cryptography instead." + ) + def check(self) -> bool: + """ + Check the consistency of an RSA private key. + + This is the Python equivalent of OpenSSL's ``RSA_check_key``. + + :return: ``True`` if key is consistent. + + :raise OpenSSL.crypto.Error: if the key is inconsistent. + + :raise TypeError: if the key is of a type which cannot be checked. + Only RSA keys can currently be checked. + """ + if self._only_public: + raise TypeError("public key only") + + if _lib.EVP_PKEY_type(self.type()) != _lib.EVP_PKEY_RSA: + raise TypeError("Only RSA keys can currently be checked.") + + rsa = _lib.EVP_PKEY_get1_RSA(self._pkey) + rsa = _ffi.gc(rsa, _lib.RSA_free) + result = _lib.RSA_check_key(rsa) + if result == 1: + return True + _raise_current_error() + + def type(self) -> int: + """ + Returns the type of the key + + :return: The type of the key. + """ + return _lib.EVP_PKEY_id(self._pkey) + + def bits(self) -> int: + """ + Returns the number of bits of the key + + :return: The number of bits of the key. + """ + return _lib.EVP_PKEY_bits(self._pkey) + + +class _EllipticCurve: + """ + A representation of a supported elliptic curve. + + @cvar _curves: :py:obj:`None` until an attempt is made to load the curves. + Thereafter, a :py:type:`set` containing :py:type:`_EllipticCurve` + instances each of which represents one curve supported by the system. + @type _curves: :py:type:`NoneType` or :py:type:`set` + """ + + _curves = None + + def __ne__(self, other: Any) -> bool: + """ + Implement cooperation with the right-hand side argument of ``!=``. + + Python 3 seems to have dropped this cooperation in this very narrow + circumstance. + """ + if isinstance(other, _EllipticCurve): + return super().__ne__(other) + return NotImplemented + + @classmethod + def _load_elliptic_curves(cls, lib: Any) -> set[_EllipticCurve]: + """ + Get the curves supported by OpenSSL. + + :param lib: The OpenSSL library binding object. + + :return: A :py:type:`set` of ``cls`` instances giving the names of the + elliptic curves the underlying library supports. + """ + num_curves = lib.EC_get_builtin_curves(_ffi.NULL, 0) + builtin_curves = _ffi.new("EC_builtin_curve[]", num_curves) + # The return value on this call should be num_curves again. We + # could check it to make sure but if it *isn't* then.. what could + # we do? Abort the whole process, I suppose...? -exarkun + lib.EC_get_builtin_curves(builtin_curves, num_curves) + return set(cls.from_nid(lib, c.nid) for c in builtin_curves) + + @classmethod + def _get_elliptic_curves(cls, lib: Any) -> set[_EllipticCurve]: + """ + Get, cache, and return the curves supported by OpenSSL. + + :param lib: The OpenSSL library binding object. + + :return: A :py:type:`set` of ``cls`` instances giving the names of the + elliptic curves the underlying library supports. + """ + if cls._curves is None: + cls._curves = cls._load_elliptic_curves(lib) + return cls._curves + + @classmethod + def from_nid(cls, lib: Any, nid: int) -> _EllipticCurve: + """ + Instantiate a new :py:class:`_EllipticCurve` associated with the given + OpenSSL NID. + + :param lib: The OpenSSL library binding object. + + :param nid: The OpenSSL NID the resulting curve object will represent. + This must be a curve NID (and not, for example, a hash NID) or + subsequent operations will fail in unpredictable ways. + :type nid: :py:class:`int` + + :return: The curve object. + """ + return cls(lib, nid, _ffi.string(lib.OBJ_nid2sn(nid)).decode("ascii")) + + def __init__(self, lib: Any, nid: int, name: str) -> None: + """ + :param _lib: The :py:mod:`cryptography` binding instance used to + interface with OpenSSL. + + :param _nid: The OpenSSL NID identifying the curve this object + represents. + :type _nid: :py:class:`int` + + :param name: The OpenSSL short name identifying the curve this object + represents. + :type name: :py:class:`unicode` + """ + self._lib = lib + self._nid = nid + self.name = name + + def __repr__(self) -> str: + return f"" + + def _to_EC_KEY(self) -> Any: + """ + Create a new OpenSSL EC_KEY structure initialized to use this curve. + + The structure is automatically garbage collected when the Python object + is garbage collected. + """ + key = self._lib.EC_KEY_new_by_curve_name(self._nid) + return _ffi.gc(key, _lib.EC_KEY_free) + + +@deprecated( + "get_elliptic_curves is deprecated. You should use the APIs in " + "cryptography instead." +) +def get_elliptic_curves() -> set[_EllipticCurve]: + """ + Return a set of objects representing the elliptic curves supported in the + OpenSSL build in use. + + The curve objects have a :py:class:`unicode` ``name`` attribute by which + they identify themselves. + + The curve objects are useful as values for the argument accepted by + :py:meth:`Context.set_tmp_ecdh` to specify which elliptical curve should be + used for ECDHE key exchange. + """ + return _EllipticCurve._get_elliptic_curves(_lib) + + +@deprecated( + "get_elliptic_curve is deprecated. You should use the APIs in " + "cryptography instead." +) +def get_elliptic_curve(name: str) -> _EllipticCurve: + """ + Return a single curve object selected by name. + + See :py:func:`get_elliptic_curves` for information about curve objects. + + :param name: The OpenSSL short name identifying the curve object to + retrieve. + :type name: :py:class:`unicode` + + If the named curve is not supported then :py:class:`ValueError` is raised. + """ + for curve in get_elliptic_curves(): + if curve.name == name: + return curve + raise ValueError("unknown curve name", name) + + +@deprecated( + "X509Name support in pyOpenSSL is deprecated. You should use the " + "APIs in cryptography." +) +@functools.total_ordering +class X509Name: + """ + An X.509 Distinguished Name. + + :ivar countryName: The country of the entity. + :ivar C: Alias for :py:attr:`countryName`. + + :ivar stateOrProvinceName: The state or province of the entity. + :ivar ST: Alias for :py:attr:`stateOrProvinceName`. + + :ivar localityName: The locality of the entity. + :ivar L: Alias for :py:attr:`localityName`. + + :ivar organizationName: The organization name of the entity. + :ivar O: Alias for :py:attr:`organizationName`. + + :ivar organizationalUnitName: The organizational unit of the entity. + :ivar OU: Alias for :py:attr:`organizationalUnitName` + + :ivar commonName: The common name of the entity. + :ivar CN: Alias for :py:attr:`commonName`. + + :ivar emailAddress: The e-mail address of the entity. + """ + + def __init__(self, name: X509Name) -> None: + """ + Create a new X509Name, copying the given X509Name instance. + + :param name: The name to copy. + :type name: :py:class:`X509Name` + """ + name = _lib.X509_NAME_dup(name._name) + self._name: Any = _ffi.gc(name, _lib.X509_NAME_free) + + def __setattr__(self, name: str, value: Any) -> None: + if name.startswith("_"): + return super().__setattr__(name, value) + + # Note: we really do not want str subclasses here, so we do not use + # isinstance. + if type(name) is not str: + raise TypeError( + f"attribute name must be string, not " + f"'{type(value).__name__:.200}'" + ) + + nid = _lib.OBJ_txt2nid(_byte_string(name)) + if nid == _lib.NID_undef: + try: + _raise_current_error() + except Error: + pass + raise AttributeError("No such attribute") + + # If there's an old entry for this NID, remove it + for i in range(_lib.X509_NAME_entry_count(self._name)): + ent = _lib.X509_NAME_get_entry(self._name, i) + ent_obj = _lib.X509_NAME_ENTRY_get_object(ent) + ent_nid = _lib.OBJ_obj2nid(ent_obj) + if nid == ent_nid: + ent = _lib.X509_NAME_delete_entry(self._name, i) + _lib.X509_NAME_ENTRY_free(ent) + break + + if isinstance(value, str): + value = value.encode("utf-8") + + add_result = _lib.X509_NAME_add_entry_by_NID( + self._name, nid, _lib.MBSTRING_UTF8, value, len(value), -1, 0 + ) + if not add_result: + _raise_current_error() + + def __getattr__(self, name: str) -> str | None: + """ + Find attribute. An X509Name object has the following attributes: + countryName (alias C), stateOrProvince (alias ST), locality (alias L), + organization (alias O), organizationalUnit (alias OU), commonName + (alias CN) and more... + """ + nid = _lib.OBJ_txt2nid(_byte_string(name)) + if nid == _lib.NID_undef: + # This is a bit weird. OBJ_txt2nid indicated failure, but it seems + # a lower level function, a2d_ASN1_OBJECT, also feels the need to + # push something onto the error queue. If we don't clean that up + # now, someone else will bump into it later and be quite confused. + # See lp#314814. + try: + _raise_current_error() + except Error: + pass + raise AttributeError("No such attribute") + + entry_index = _lib.X509_NAME_get_index_by_NID(self._name, nid, -1) + if entry_index == -1: + return None + + entry = _lib.X509_NAME_get_entry(self._name, entry_index) + data = _lib.X509_NAME_ENTRY_get_data(entry) + + result_buffer = _ffi.new("unsigned char**") + data_length = _lib.ASN1_STRING_to_UTF8(result_buffer, data) + _openssl_assert(data_length >= 0) + + try: + result = _ffi.buffer(result_buffer[0], data_length)[:].decode( + "utf-8" + ) + finally: + # XXX untested + _lib.OPENSSL_free(result_buffer[0]) + return result + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, X509Name): + return NotImplemented + + return _lib.X509_NAME_cmp(self._name, other._name) == 0 + + def __lt__(self, other: Any) -> bool: + if not isinstance(other, X509Name): + return NotImplemented + + return _lib.X509_NAME_cmp(self._name, other._name) < 0 + + def __repr__(self) -> str: + """ + String representation of an X509Name + """ + result_buffer = _ffi.new("char[]", 512) + format_result = _lib.X509_NAME_oneline( + self._name, result_buffer, len(result_buffer) + ) + _openssl_assert(format_result != _ffi.NULL) + + return "".format( + _ffi.string(result_buffer).decode("utf-8"), + ) + + def hash(self) -> int: + """ + Return an integer representation of the first four bytes of the + MD5 digest of the DER representation of the name. + + This is the Python equivalent of OpenSSL's ``X509_NAME_hash``. + + :return: The (integer) hash of this name. + :rtype: :py:class:`int` + """ + return _lib.X509_NAME_hash(self._name) + + def der(self) -> bytes: + """ + Return the DER encoding of this name. + + :return: The DER encoded form of this name. + :rtype: :py:class:`bytes` + """ + result_buffer = _ffi.new("unsigned char**") + encode_result = _lib.i2d_X509_NAME(self._name, result_buffer) + _openssl_assert(encode_result >= 0) + + string_result = _ffi.buffer(result_buffer[0], encode_result)[:] + _lib.OPENSSL_free(result_buffer[0]) + return string_result + + def get_components(self) -> list[tuple[bytes, bytes]]: + """ + Returns the components of this name, as a sequence of 2-tuples. + + :return: The components of this name. + :rtype: :py:class:`list` of ``name, value`` tuples. + """ + result = [] + for i in range(_lib.X509_NAME_entry_count(self._name)): + ent = _lib.X509_NAME_get_entry(self._name, i) + + fname = _lib.X509_NAME_ENTRY_get_object(ent) + fval = _lib.X509_NAME_ENTRY_get_data(ent) + + nid = _lib.OBJ_obj2nid(fname) + name = _lib.OBJ_nid2sn(nid) + + # ffi.string does not handle strings containing NULL bytes + # (which may have been generated by old, broken software) + value = _ffi.buffer( + _lib.ASN1_STRING_get0_data(fval), _lib.ASN1_STRING_length(fval) + )[:] + result.append((_ffi.string(name), value)) + + return result + + +class X509: + """ + An X.509 certificate. + """ + + def __init__(self) -> None: + x509 = _lib.X509_new() + _openssl_assert(x509 != _ffi.NULL) + self._x509 = _ffi.gc(x509, _lib.X509_free) + + self._issuer_invalidator = _X509NameInvalidator() + self._subject_invalidator = _X509NameInvalidator() + + @classmethod + def _from_raw_x509_ptr(cls, x509: Any) -> X509: + cert = cls.__new__(cls) + cert._x509 = _ffi.gc(x509, _lib.X509_free) + cert._issuer_invalidator = _X509NameInvalidator() + cert._subject_invalidator = _X509NameInvalidator() + return cert + + def to_cryptography(self) -> x509.Certificate: + """ + Export as a ``cryptography`` certificate. + + :rtype: ``cryptography.x509.Certificate`` + + .. versionadded:: 17.1.0 + """ + from cryptography.x509 import load_der_x509_certificate + + der = dump_certificate(FILETYPE_ASN1, self) + return load_der_x509_certificate(der) + + @classmethod + def from_cryptography(cls, crypto_cert: x509.Certificate) -> X509: + """ + Construct based on a ``cryptography`` *crypto_cert*. + + :param crypto_key: A ``cryptography`` X.509 certificate. + :type crypto_key: ``cryptography.x509.Certificate`` + + :rtype: X509 + + .. versionadded:: 17.1.0 + """ + if not isinstance(crypto_cert, x509.Certificate): + raise TypeError("Must be a certificate") + + from cryptography.hazmat.primitives.serialization import Encoding + + der = crypto_cert.public_bytes(Encoding.DER) + return load_certificate(FILETYPE_ASN1, der) + + @deprecated( + "X509.set_version is deprecated. You should use " + "cryptography's CertificateBuilder instead." + ) + def set_version(self, version: int) -> None: + """ + Set the version number of the certificate. Note that the + version value is zero-based, eg. a value of 0 is V1. + + :param version: The version number of the certificate. + :type version: :py:class:`int` + + :return: ``None`` + """ + if not isinstance(version, int): + raise TypeError("version must be an integer") + + _openssl_assert(_lib.X509_set_version(self._x509, version) == 1) + + def get_version(self) -> int: + """ + Return the version number of the certificate. + + :return: The version number of the certificate. + :rtype: :py:class:`int` + """ + return _lib.X509_get_version(self._x509) + + def get_pubkey(self) -> PKey: + """ + Get the public key of the certificate. + + :return: The public key. + :rtype: :py:class:`PKey` + """ + pkey = PKey.__new__(PKey) + pkey._pkey = _lib.X509_get_pubkey(self._x509) + if pkey._pkey == _ffi.NULL: + _raise_current_error() + pkey._pkey = _ffi.gc(pkey._pkey, _lib.EVP_PKEY_free) + pkey._only_public = True + return pkey + + @deprecated( + "X509.set_pubkey is deprecated. You should use " + "cryptography's CertificateBuilder instead." + ) + def set_pubkey(self, pkey: PKey) -> None: + """ + Set the public key of the certificate. + + :param pkey: The public key. + :type pkey: :py:class:`PKey` + + :return: :py:data:`None` + """ + if not isinstance(pkey, PKey): + raise TypeError("pkey must be a PKey instance") + + set_result = _lib.X509_set_pubkey(self._x509, pkey._pkey) + _openssl_assert(set_result == 1) + + @deprecated( + "X509.sign is deprecated. You should use " + "cryptography's CertificateBuilder instead." + ) + def sign(self, pkey: PKey, digest: str) -> None: + """ + Sign the certificate with this key and digest type. + + :param pkey: The key to sign with. + :type pkey: :py:class:`PKey` + + :param digest: The name of the message digest to use. + :type digest: :py:class:`str` + + :return: :py:data:`None` + """ + if not isinstance(pkey, PKey): + raise TypeError("pkey must be a PKey instance") + + if pkey._only_public: + raise ValueError("Key only has public part") + + if not pkey._initialized: + raise ValueError("Key is uninitialized") + + evp_md = _lib.EVP_get_digestbyname(_byte_string(digest)) + if evp_md == _ffi.NULL: + raise ValueError("No such digest method") + + sign_result = _lib.X509_sign(self._x509, pkey._pkey, evp_md) + _openssl_assert(sign_result > 0) + + def get_signature_algorithm(self) -> bytes: + """ + Return the signature algorithm used in the certificate. + + :return: The name of the algorithm. + :rtype: :py:class:`bytes` + + :raises ValueError: If the signature algorithm is undefined. + + .. versionadded:: 0.13 + """ + sig_alg = _lib.X509_get0_tbs_sigalg(self._x509) + alg = _ffi.new("ASN1_OBJECT **") + _lib.X509_ALGOR_get0(alg, _ffi.NULL, _ffi.NULL, sig_alg) + nid = _lib.OBJ_obj2nid(alg[0]) + if nid == _lib.NID_undef: + raise ValueError("Undefined signature algorithm") + return _ffi.string(_lib.OBJ_nid2ln(nid)) + + def digest(self, digest_name: str) -> bytes: + """ + Return the digest of the X509 object. + + :param digest_name: The name of the digest algorithm to use. + :type digest_name: :py:class:`str` + + :return: The digest of the object, formatted as + :py:const:`b":"`-delimited hex pairs. + :rtype: :py:class:`bytes` + """ + digest = _lib.EVP_get_digestbyname(_byte_string(digest_name)) + if digest == _ffi.NULL: + raise ValueError("No such digest method") + + result_buffer = _ffi.new("unsigned char[]", _lib.EVP_MAX_MD_SIZE) + result_length = _ffi.new("unsigned int[]", 1) + result_length[0] = len(result_buffer) + + digest_result = _lib.X509_digest( + self._x509, digest, result_buffer, result_length + ) + _openssl_assert(digest_result == 1) + + return b":".join( + [ + b16encode(ch).upper() + for ch in _ffi.buffer(result_buffer, result_length[0]) + ] + ) + + def subject_name_hash(self) -> int: + """ + Return the hash of the X509 subject. + + :return: The hash of the subject. + :rtype: :py:class:`int` + """ + return _lib.X509_subject_name_hash(self._x509) + + @deprecated( + "X509.set_serial_number is deprecated. You should use " + "cryptography's CertificateBuilder instead." + ) + def set_serial_number(self, serial: int) -> None: + """ + Set the serial number of the certificate. + + :param serial: The new serial number. + :type serial: :py:class:`int` + + :return: :py:data`None` + """ + if not isinstance(serial, int): + raise TypeError("serial must be an integer") + + hex_serial = hex(serial)[2:] + hex_serial_bytes = hex_serial.encode("ascii") + + bignum_serial = _ffi.new("BIGNUM**") + + # BN_hex2bn stores the result in &bignum. + result = _lib.BN_hex2bn(bignum_serial, hex_serial_bytes) + _openssl_assert(result != _ffi.NULL) + + asn1_serial = _lib.BN_to_ASN1_INTEGER(bignum_serial[0], _ffi.NULL) + _lib.BN_free(bignum_serial[0]) + _openssl_assert(asn1_serial != _ffi.NULL) + asn1_serial = _ffi.gc(asn1_serial, _lib.ASN1_INTEGER_free) + set_result = _lib.X509_set_serialNumber(self._x509, asn1_serial) + _openssl_assert(set_result == 1) + + def get_serial_number(self) -> int: + """ + Return the serial number of this certificate. + + :return: The serial number. + :rtype: int + """ + asn1_serial = _lib.X509_get_serialNumber(self._x509) + bignum_serial = _lib.ASN1_INTEGER_to_BN(asn1_serial, _ffi.NULL) + try: + hex_serial = _lib.BN_bn2hex(bignum_serial) + try: + hexstring_serial = _ffi.string(hex_serial) + serial = int(hexstring_serial, 16) + return serial + finally: + _lib.OPENSSL_free(hex_serial) + finally: + _lib.BN_free(bignum_serial) + + @deprecated( + "X509.gmtime_adj_notAfter is deprecated. You should use " + "cryptography's CertificateBuilder instead." + ) + def gmtime_adj_notAfter(self, amount: int) -> None: + """ + Adjust the time stamp on which the certificate stops being valid. + + :param int amount: The number of seconds by which to adjust the + timestamp. + :return: ``None`` + """ + if not isinstance(amount, int): + raise TypeError("amount must be an integer") + + notAfter = _lib.X509_getm_notAfter(self._x509) + _lib.X509_gmtime_adj(notAfter, amount) + + @deprecated( + "X509.gmtime_adj_notBefore is deprecated. You should use " + "cryptography's CertificateBuilder instead." + ) + def gmtime_adj_notBefore(self, amount: int) -> None: + """ + Adjust the timestamp on which the certificate starts being valid. + + :param amount: The number of seconds by which to adjust the timestamp. + :return: ``None`` + """ + if not isinstance(amount, int): + raise TypeError("amount must be an integer") + + notBefore = _lib.X509_getm_notBefore(self._x509) + _lib.X509_gmtime_adj(notBefore, amount) + + def has_expired(self) -> bool: + """ + Check whether the certificate has expired. + + :return: ``True`` if the certificate has expired, ``False`` otherwise. + :rtype: bool + """ + time_bytes = self.get_notAfter() + if time_bytes is None: + raise ValueError("Unable to determine notAfter") + time_string = time_bytes.decode("utf-8") + not_after = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") + + UTC = datetime.timezone.utc + utcnow = datetime.datetime.now(UTC).replace(tzinfo=None) + return not_after < utcnow + + def _get_boundary_time(self, which: Any) -> bytes | None: + return _get_asn1_time(which(self._x509)) + + def get_notBefore(self) -> bytes | None: + """ + Get the timestamp at which the certificate starts being valid. + + The timestamp is formatted as an ASN.1 TIME:: + + YYYYMMDDhhmmssZ + + :return: A timestamp string, or ``None`` if there is none. + :rtype: bytes or NoneType + """ + return self._get_boundary_time(_lib.X509_getm_notBefore) + + def _set_boundary_time( + self, which: Callable[..., Any], when: bytes + ) -> None: + return _set_asn1_time(which(self._x509), when) + + @deprecated( + "X509.set_notBefore is deprecated. You should use " + "cryptography's CertificateBuilder instead." + ) + def set_notBefore(self, when: bytes) -> None: + """ + Set the timestamp at which the certificate starts being valid. + + The timestamp is formatted as an ASN.1 TIME:: + + YYYYMMDDhhmmssZ + + :param bytes when: A timestamp string. + :return: ``None`` + """ + return self._set_boundary_time(_lib.X509_getm_notBefore, when) + + def get_notAfter(self) -> bytes | None: + """ + Get the timestamp at which the certificate stops being valid. + + The timestamp is formatted as an ASN.1 TIME:: + + YYYYMMDDhhmmssZ + + :return: A timestamp string, or ``None`` if there is none. + :rtype: bytes or NoneType + """ + return self._get_boundary_time(_lib.X509_getm_notAfter) + + @deprecated( + "X509.set_notAfter is deprecated. You should use " + "cryptography's CertificateBuilder instead." + ) + def set_notAfter(self, when: bytes) -> None: + """ + Set the timestamp at which the certificate stops being valid. + + The timestamp is formatted as an ASN.1 TIME:: + + YYYYMMDDhhmmssZ + + :param bytes when: A timestamp string. + :return: ``None`` + """ + return self._set_boundary_time(_lib.X509_getm_notAfter, when) + + def _get_name(self, which: Any) -> X509Name: + # Bypass X509Name.__new__, which warns that X509Name is deprecated; + # callers that should warn are decorated individually. + name = object.__new__(X509Name) + name._name = which(self._x509) + _openssl_assert(name._name != _ffi.NULL) + + # The name is owned by the X509 structure. As long as the X509Name + # Python object is alive, keep the X509 Python object alive. + name._owner = self + + return name + + def _set_name(self, which: Any, name: X509Name) -> None: + if not isinstance(name, X509Name): + raise TypeError("name must be an X509Name") + set_result = which(self._x509, name._name) + _openssl_assert(set_result == 1) + + @deprecated( + "X509.get_issuer is deprecated. You should use " + "cryptography's X.509 APIs instead." + ) + def get_issuer(self) -> X509Name: + """ + Return the issuer of this certificate. + + This creates a new :class:`X509Name` that wraps the underlying issuer + name field on the certificate. Modifying it will modify the underlying + certificate, and will have the effect of modifying any other + :class:`X509Name` that refers to this issuer. + + :return: The issuer of this certificate. + :rtype: :class:`X509Name` + """ + name = self._get_name(_lib.X509_get_issuer_name) + self._issuer_invalidator.add(name) + return name + + @deprecated( + "X509.set_issuer is deprecated. You should use " + "cryptography's CertificateBuilder instead." + ) + def set_issuer(self, issuer: X509Name) -> None: + """ + Set the issuer of this certificate. + + :param issuer: The issuer. + :type issuer: :py:class:`X509Name` + + :return: ``None`` + """ + self._set_name(_lib.X509_set_issuer_name, issuer) + self._issuer_invalidator.clear() + + @deprecated( + "X509.get_subject is deprecated. You should use " + "cryptography's X.509 APIs instead." + ) + def get_subject(self) -> X509Name: + """ + Return the subject of this certificate. + + This creates a new :class:`X509Name` that wraps the underlying subject + name field on the certificate. Modifying it will modify the underlying + certificate, and will have the effect of modifying any other + :class:`X509Name` that refers to this subject. + + :return: The subject of this certificate. + :rtype: :class:`X509Name` + """ + name = self._get_name(_lib.X509_get_subject_name) + self._subject_invalidator.add(name) + return name + + @deprecated( + "X509.set_subject is deprecated. You should use " + "cryptography's CertificateBuilder instead." + ) + def set_subject(self, subject: X509Name) -> None: + """ + Set the subject of this certificate. + + :param subject: The subject. + :type subject: :py:class:`X509Name` + + :return: ``None`` + """ + self._set_name(_lib.X509_set_subject_name, subject) + self._subject_invalidator.clear() + + def get_extension_count(self) -> int: + """ + Get the number of extensions on this certificate. + + :return: The number of extensions. + :rtype: :py:class:`int` + + .. versionadded:: 0.12 + """ + return _lib.X509_get_ext_count(self._x509) + + +class X509StoreFlags: + """ + Flags for X509 verification, used to change the behavior of + :class:`X509Store`. + + See `OpenSSL Verification Flags`_ for details. + + .. _OpenSSL Verification Flags: + https://www.openssl.org/docs/manmaster/man3/X509_VERIFY_PARAM_set_flags.html + """ + + CRL_CHECK: int = _lib.X509_V_FLAG_CRL_CHECK + CRL_CHECK_ALL: int = _lib.X509_V_FLAG_CRL_CHECK_ALL + IGNORE_CRITICAL: int = _lib.X509_V_FLAG_IGNORE_CRITICAL + X509_STRICT: int = _lib.X509_V_FLAG_X509_STRICT + ALLOW_PROXY_CERTS: int = _lib.X509_V_FLAG_ALLOW_PROXY_CERTS + POLICY_CHECK: int = _lib.X509_V_FLAG_POLICY_CHECK + EXPLICIT_POLICY: int = _lib.X509_V_FLAG_EXPLICIT_POLICY + INHIBIT_MAP: int = _lib.X509_V_FLAG_INHIBIT_MAP + CHECK_SS_SIGNATURE: int = _lib.X509_V_FLAG_CHECK_SS_SIGNATURE + PARTIAL_CHAIN: int = _lib.X509_V_FLAG_PARTIAL_CHAIN + + +class X509Store: + """ + An X.509 store. + + An X.509 store is used to describe a context in which to verify a + certificate. A description of a context may include a set of certificates + to trust, a set of certificate revocation lists, verification flags and + more. + + An X.509 store, being only a description, cannot be used by itself to + verify a certificate. To carry out the actual verification process, see + :class:`X509StoreContext`. + """ + + def __init__(self) -> None: + store = _lib.X509_STORE_new() + self._store = _ffi.gc(store, _lib.X509_STORE_free) + + def add_cert(self, cert: X509) -> None: + """ + Adds a trusted certificate to this store. + + Adding a certificate with this method adds this certificate as a + *trusted* certificate. + + :param X509 cert: The certificate to add to this store. + + :raises TypeError: If the certificate is not an :class:`X509`. + + :raises OpenSSL.crypto.Error: If OpenSSL was unhappy with your + certificate. + + :return: ``None`` if the certificate was added successfully. + """ + if not isinstance(cert, X509): + raise TypeError() + + res = _lib.X509_STORE_add_cert(self._store, cert._x509) + _openssl_assert(res == 1) + + def add_crl(self, crl: x509.CertificateRevocationList) -> None: + """ + Add a certificate revocation list to this store. + + The certificate revocation lists added to a store will only be used if + the associated flags are configured to check certificate revocation + lists. + + .. versionadded:: 16.1.0 + + :param crl: The certificate revocation list to add to this store. + :type crl: ``cryptography.x509.CertificateRevocationList`` + :return: ``None`` if the certificate revocation list was added + successfully. + """ + if isinstance(crl, x509.CertificateRevocationList): + from cryptography.hazmat.primitives.serialization import Encoding + + bio = _new_mem_buf(crl.public_bytes(Encoding.DER)) + openssl_crl = _lib.d2i_X509_CRL_bio(bio, _ffi.NULL) + _openssl_assert(openssl_crl != _ffi.NULL) + crl = _ffi.gc(openssl_crl, _lib.X509_CRL_free) + else: + raise TypeError( + "CRL must be of type " + "cryptography.x509.CertificateRevocationList" + ) + + _openssl_assert(_lib.X509_STORE_add_crl(self._store, crl) != 0) + + def set_flags(self, flags: int) -> None: + """ + Set verification flags to this store. + + Verification flags can be combined by oring them together. + + .. note:: + + Setting a verification flag sometimes requires clients to add + additional information to the store, otherwise a suitable error will + be raised. + + For example, in setting flags to enable CRL checking a + suitable CRL must be added to the store otherwise an error will be + raised. + + .. versionadded:: 16.1.0 + + :param int flags: The verification flags to set on this store. + See :class:`X509StoreFlags` for available constants. + :return: ``None`` if the verification flags were successfully set. + """ + _openssl_assert(_lib.X509_STORE_set_flags(self._store, flags) != 0) + + def set_time(self, vfy_time: datetime.datetime) -> None: + """ + Set the time against which the certificates are verified. + + Normally the current time is used. + + .. note:: + + For example, you can determine if a certificate was valid at a given + time. + + .. versionadded:: 17.0.0 + + :param datetime vfy_time: The verification time to set on this store. + :return: ``None`` if the verification time was successfully set. + """ + param = _lib.X509_VERIFY_PARAM_new() + param = _ffi.gc(param, _lib.X509_VERIFY_PARAM_free) + + _lib.X509_VERIFY_PARAM_set_time( + param, calendar.timegm(vfy_time.timetuple()) + ) + _openssl_assert(_lib.X509_STORE_set1_param(self._store, param) != 0) + + def load_locations( + self, + cafile: StrOrBytesPath | None, + capath: StrOrBytesPath | None = None, + ) -> None: + """ + Let X509Store know where we can find trusted certificates for the + certificate chain. Note that the certificates have to be in PEM + format. + + If *capath* is passed, it must be a directory prepared using the + ``c_rehash`` tool included with OpenSSL. Either, but not both, of + *cafile* or *capath* may be ``None``. + + .. note:: + + Both *cafile* and *capath* may be set simultaneously. + + Call this method multiple times to add more than one location. + For example, CA certificates, and certificate revocation list bundles + may be passed in *cafile* in subsequent calls to this method. + + .. versionadded:: 20.0 + + :param cafile: In which file we can find the certificates (``bytes`` or + ``unicode``). + :param capath: In which directory we can find the certificates + (``bytes`` or ``unicode``). + + :return: ``None`` if the locations were set successfully. + + :raises OpenSSL.crypto.Error: If both *cafile* and *capath* is ``None`` + or the locations could not be set for any reason. + + """ + if cafile is None: + cafile = _ffi.NULL + else: + cafile = _path_bytes(cafile) + + if capath is None: + capath = _ffi.NULL + else: + capath = _path_bytes(capath) + + load_result = _lib.X509_STORE_load_locations( + self._store, cafile, capath + ) + if not load_result: + _raise_current_error() + + +class X509StoreContextError(Exception): + """ + An exception raised when an error occurred while verifying a certificate + using `OpenSSL.X509StoreContext.verify_certificate`. + + :ivar certificate: The certificate which caused verificate failure. + :type certificate: :class:`X509` + """ + + def __init__( + self, message: str, errors: list[Any], certificate: X509 + ) -> None: + super().__init__(message) + self.errors = errors + self.certificate = certificate + + +class X509StoreContext: + """ + An X.509 store context. + + An X.509 store context is used to carry out the actual verification process + of a certificate in a described context. For describing such a context, see + :class:`X509Store`. + + :param X509Store store: The certificates which will be trusted for the + purposes of any verifications. + :param X509 certificate: The certificate to be verified. + :param chain: List of untrusted certificates that may be used for building + the certificate chain. May be ``None``. + :type chain: :class:`list` of :class:`X509` + """ + + def __init__( + self, + store: X509Store, + certificate: X509, + chain: Sequence[X509] | None = None, + ) -> None: + self._store = store + self._cert = certificate + self._chain = self._build_certificate_stack(chain) + + @staticmethod + def _build_certificate_stack( + certificates: Sequence[X509] | None, + ) -> None: + def cleanup(s: Any) -> None: + # Equivalent to sk_X509_pop_free, but we don't + # currently have a CFFI binding for that available + for i in range(_lib.sk_X509_num(s)): + x = _lib.sk_X509_value(s, i) + _lib.X509_free(x) + _lib.sk_X509_free(s) + + if certificates is None or len(certificates) == 0: + return _ffi.NULL + + stack = _lib.sk_X509_new_null() + _openssl_assert(stack != _ffi.NULL) + stack = _ffi.gc(stack, cleanup) + + for cert in certificates: + if not isinstance(cert, X509): + raise TypeError("One of the elements is not an X509 instance") + + _openssl_assert(_lib.X509_up_ref(cert._x509) > 0) + if _lib.sk_X509_push(stack, cert._x509) <= 0: + _lib.X509_free(cert._x509) + _raise_current_error() + + return stack + + @staticmethod + def _exception_from_context(store_ctx: Any) -> X509StoreContextError: + """ + Convert an OpenSSL native context error failure into a Python + exception. + + When a call to native OpenSSL X509_verify_cert fails, additional + information about the failure can be obtained from the store context. + """ + message = _ffi.string( + _lib.X509_verify_cert_error_string( + _lib.X509_STORE_CTX_get_error(store_ctx) + ) + ).decode("utf-8") + errors = [ + _lib.X509_STORE_CTX_get_error(store_ctx), + _lib.X509_STORE_CTX_get_error_depth(store_ctx), + message, + ] + # A context error should always be associated with a certificate, so we + # expect this call to never return :class:`None`. + _x509 = _lib.X509_STORE_CTX_get_current_cert(store_ctx) + _cert = _lib.X509_dup(_x509) + pycert = X509._from_raw_x509_ptr(_cert) + return X509StoreContextError(message, errors, pycert) + + def _verify_certificate(self) -> Any: + """ + Verifies the certificate and runs an X509_STORE_CTX containing the + results. + + :raises X509StoreContextError: If an error occurred when validating a + certificate in the context. Sets ``certificate`` attribute to + indicate which certificate caused the error. + """ + store_ctx = _lib.X509_STORE_CTX_new() + _openssl_assert(store_ctx != _ffi.NULL) + store_ctx = _ffi.gc(store_ctx, _lib.X509_STORE_CTX_free) + + ret = _lib.X509_STORE_CTX_init( + store_ctx, self._store._store, self._cert._x509, self._chain + ) + _openssl_assert(ret == 1) + + ret = _lib.X509_verify_cert(store_ctx) + if ret <= 0: + raise self._exception_from_context(store_ctx) + + return store_ctx + + def set_store(self, store: X509Store) -> None: + """ + Set the context's X.509 store. + + .. versionadded:: 0.15 + + :param X509Store store: The store description which will be used for + the purposes of any *future* verifications. + """ + self._store = store + + def verify_certificate(self) -> None: + """ + Verify a certificate in a context. + + .. versionadded:: 0.15 + + :raises X509StoreContextError: If an error occurred when validating a + certificate in the context. Sets ``certificate`` attribute to + indicate which certificate caused the error. + """ + self._verify_certificate() + + def get_verified_chain(self) -> list[X509]: + """ + Verify a certificate in a context and return the complete validated + chain. + + :raises X509StoreContextError: If an error occurred when validating a + certificate in the context. Sets ``certificate`` attribute to + indicate which certificate caused the error. + + .. versionadded:: 20.0 + """ + store_ctx = self._verify_certificate() + + # Note: X509_STORE_CTX_get1_chain returns a deep copy of the chain. + cert_stack = _lib.X509_STORE_CTX_get1_chain(store_ctx) + _openssl_assert(cert_stack != _ffi.NULL) + + result = [] + for i in range(_lib.sk_X509_num(cert_stack)): + cert = _lib.sk_X509_value(cert_stack, i) + _openssl_assert(cert != _ffi.NULL) + pycert = X509._from_raw_x509_ptr(cert) + result.append(pycert) + + # Free the stack but not the members which are freed by the X509 class. + _lib.sk_X509_free(cert_stack) + return result + + +def load_certificate(type: int, buffer: bytes) -> X509: + """ + Load a certificate (X509) from the string *buffer* encoded with the + type *type*. + + :param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1) + + :param bytes buffer: The buffer the certificate is stored in + + :return: The X509 object + """ + if isinstance(buffer, str): + buffer = buffer.encode("ascii") + + bio = _new_mem_buf(buffer) + + if type == FILETYPE_PEM: + x509 = _lib.PEM_read_bio_X509(bio, _ffi.NULL, _ffi.NULL, _ffi.NULL) + elif type == FILETYPE_ASN1: + x509 = _lib.d2i_X509_bio(bio, _ffi.NULL) + else: + raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1") + + if x509 == _ffi.NULL: + _raise_current_error() + + return X509._from_raw_x509_ptr(x509) + + +def dump_certificate(type: int, cert: X509) -> bytes: + """ + Dump the certificate *cert* into a buffer string encoded with the type + *type*. + + :param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1, or + FILETYPE_TEXT) + :param cert: The certificate to dump + :return: The buffer with the dumped certificate in + """ + bio = _new_mem_buf() + + if type == FILETYPE_PEM: + result_code = _lib.PEM_write_bio_X509(bio, cert._x509) + elif type == FILETYPE_ASN1: + result_code = _lib.i2d_X509_bio(bio, cert._x509) + elif type == FILETYPE_TEXT: + result_code = _lib.X509_print_ex(bio, cert._x509, 0, 0) + else: + raise ValueError( + "type argument must be FILETYPE_PEM, FILETYPE_ASN1, or " + "FILETYPE_TEXT" + ) + + _openssl_assert(result_code == 1) + return _bio_to_string(bio) + + +def dump_publickey(type: int, pkey: PKey) -> bytes: + """ + Dump a public key to a buffer. + + :param type: The file type (one of :data:`FILETYPE_PEM` or + :data:`FILETYPE_ASN1`). + :param PKey pkey: The public key to dump + :return: The buffer with the dumped key in it. + :rtype: bytes + """ + bio = _new_mem_buf() + if type == FILETYPE_PEM: + write_bio = _lib.PEM_write_bio_PUBKEY + elif type == FILETYPE_ASN1: + write_bio = _lib.i2d_PUBKEY_bio + else: + raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1") + + result_code = write_bio(bio, pkey._pkey) + if result_code != 1: # pragma: no cover + _raise_current_error() + + return _bio_to_string(bio) + + +def dump_privatekey( + type: int, + pkey: PKey, + cipher: str | None = None, + passphrase: PassphraseCallableT | None = None, +) -> bytes: + """ + Dump the private key *pkey* into a buffer string encoded with the type + *type*. Optionally (if *type* is :const:`FILETYPE_PEM`) encrypting it + using *cipher* and *passphrase*. + + :param type: The file type (one of :const:`FILETYPE_PEM`, + :const:`FILETYPE_ASN1`, or :const:`FILETYPE_TEXT`) + :param PKey pkey: The PKey to dump + :param cipher: (optional) if encrypted PEM format, the cipher to use + :param passphrase: (optional) if encrypted PEM format, this can be either + the passphrase to use, or a callback for providing the passphrase. + + :return: The buffer with the dumped key in + :rtype: bytes + + .. deprecated:: 26.3.0 + Use the serialization APIs on ``cryptography`` private key types + instead. + """ + bio = _new_mem_buf() + + if not isinstance(pkey, PKey): + raise TypeError("pkey must be a PKey") + + if cipher is not None: + if passphrase is None: + raise TypeError( + "if a value is given for cipher " + "one must also be given for passphrase" + ) + cipher_obj = _lib.EVP_get_cipherbyname(_byte_string(cipher)) + if cipher_obj == _ffi.NULL: + raise ValueError("Invalid cipher name") + else: + cipher_obj = _ffi.NULL + + helper = _PassphraseHelper(type, passphrase) + if type == FILETYPE_PEM: + result_code = _lib.PEM_write_bio_PrivateKey( + bio, + pkey._pkey, + cipher_obj, + _ffi.NULL, + 0, + helper.callback, + helper.callback_args, + ) + helper.raise_if_problem() + elif type == FILETYPE_ASN1: + result_code = _lib.i2d_PrivateKey_bio(bio, pkey._pkey) + elif type == FILETYPE_TEXT: + if _lib.EVP_PKEY_id(pkey._pkey) != _lib.EVP_PKEY_RSA: + raise TypeError("Only RSA keys are supported for FILETYPE_TEXT") + + rsa = _ffi.gc(_lib.EVP_PKEY_get1_RSA(pkey._pkey), _lib.RSA_free) + result_code = _lib.RSA_print(bio, rsa, 0) + else: + raise ValueError( + "type argument must be FILETYPE_PEM, FILETYPE_ASN1, or " + "FILETYPE_TEXT" + ) + + _openssl_assert(result_code != 0) + + return _bio_to_string(bio) + + +_dump_privatekey_internal = dump_privatekey + +utils.deprecated( + dump_privatekey, + __name__, + ( + "dump_privatekey is deprecated. You should use the APIs in " + "cryptography." + ), + DeprecationWarning, + name="dump_privatekey", +) + + +class _PassphraseHelper: + def __init__( + self, + type: int, + passphrase: PassphraseCallableT | None, + more_args: bool = False, + truncate: bool = False, + ) -> None: + if type != FILETYPE_PEM and passphrase is not None: + raise ValueError( + "only FILETYPE_PEM key format supports encryption" + ) + self._passphrase = passphrase + self._more_args = more_args + self._truncate = truncate + self._problems: list[Exception] = [] + + @property + def callback(self) -> Any: + if self._passphrase is None: + return _ffi.NULL + elif isinstance(self._passphrase, bytes) or callable(self._passphrase): + return _ffi.callback("pem_password_cb", self._read_passphrase) + else: + raise TypeError( + "Last argument must be a byte string or a callable." + ) + + @property + def callback_args(self) -> Any: + if self._passphrase is None: + return _ffi.NULL + elif isinstance(self._passphrase, bytes) or callable(self._passphrase): + return _ffi.NULL + else: + raise TypeError( + "Last argument must be a byte string or a callable." + ) + + def raise_if_problem(self, exceptionType: type[Exception] = Error) -> None: + if self._problems: + # Flush the OpenSSL error queue + try: + _exception_from_error_queue(exceptionType) + except exceptionType: + pass + + raise self._problems.pop(0) + + def _read_passphrase( + self, buf: Any, size: int, rwflag: Any, userdata: Any + ) -> int: + try: + if callable(self._passphrase): + if self._more_args: + result = self._passphrase(size, rwflag, userdata) + else: + result = self._passphrase(rwflag) + else: + assert self._passphrase is not None + result = self._passphrase + if not isinstance(result, bytes): + raise ValueError("Bytes expected") + if len(result) > size: + if self._truncate: + result = result[:size] + else: + raise ValueError( + "passphrase returned by callback is too long" + ) + for i in range(len(result)): + buf[i] = result[i : i + 1] + return len(result) + except Exception as e: + self._problems.append(e) + return 0 + + +def load_publickey(type: int, buffer: str | bytes) -> PKey: + """ + Load a public key from a buffer. + + :param type: The file type (one of :data:`FILETYPE_PEM`, + :data:`FILETYPE_ASN1`). + :param buffer: The buffer the key is stored in. + :type buffer: A Python string object, either unicode or bytestring. + :return: The PKey object. + :rtype: :class:`PKey` + """ + if isinstance(buffer, str): + buffer = buffer.encode("ascii") + + bio = _new_mem_buf(buffer) + + if type == FILETYPE_PEM: + evp_pkey = _lib.PEM_read_bio_PUBKEY( + bio, _ffi.NULL, _ffi.NULL, _ffi.NULL + ) + elif type == FILETYPE_ASN1: + evp_pkey = _lib.d2i_PUBKEY_bio(bio, _ffi.NULL) + else: + raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1") + + if evp_pkey == _ffi.NULL: + _raise_current_error() + + pkey = PKey.__new__(PKey) + pkey._pkey = _ffi.gc(evp_pkey, _lib.EVP_PKEY_free) + pkey._only_public = True + return pkey + + +def load_privatekey( + type: int, + buffer: str | bytes, + passphrase: PassphraseCallableT | None = None, +) -> PKey: + """ + Load a private key (PKey) from the string *buffer* encoded with the type + *type*. + + :param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1) + :param buffer: The buffer the key is stored in + :param passphrase: (optional) if encrypted PEM format, this can be + either the passphrase to use, or a callback for + providing the passphrase. + + :return: The PKey object + """ + if isinstance(buffer, str): + buffer = buffer.encode("ascii") + + bio = _new_mem_buf(buffer) + + helper = _PassphraseHelper(type, passphrase) + if type == FILETYPE_PEM: + evp_pkey = _lib.PEM_read_bio_PrivateKey( + bio, _ffi.NULL, helper.callback, helper.callback_args + ) + helper.raise_if_problem() + elif type == FILETYPE_ASN1: + evp_pkey = _lib.d2i_PrivateKey_bio(bio, _ffi.NULL) + else: + raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1") + + if evp_pkey == _ffi.NULL: + _raise_current_error() + + pkey = PKey.__new__(PKey) + pkey._pkey = _ffi.gc(evp_pkey, _lib.EVP_PKEY_free) + return pkey diff --git a/src/OpenSSL/debug.py b/src/OpenSSL/debug.py new file mode 100644 index 000000000..e0ed3f81d --- /dev/null +++ b/src/OpenSSL/debug.py @@ -0,0 +1,40 @@ +import ssl +import sys + +import cffi +import cryptography + +import OpenSSL.SSL + +from . import version + +_env_info = """\ +pyOpenSSL: {pyopenssl} +cryptography: {cryptography} +cffi: {cffi} +cryptography's compiled against OpenSSL: {crypto_openssl_compile} +cryptography's linked OpenSSL: {crypto_openssl_link} +Python's OpenSSL: {python_openssl} +Python executable: {python} +Python version: {python_version} +Platform: {platform} +sys.path: {sys_path}""".format( + pyopenssl=version.__version__, + crypto_openssl_compile=OpenSSL._util.ffi.string( + OpenSSL._util.lib.OPENSSL_VERSION_TEXT, + ).decode("ascii"), + crypto_openssl_link=OpenSSL.SSL.SSLeay_version( + OpenSSL.SSL.SSLEAY_VERSION + ).decode("ascii"), + python_openssl=getattr(ssl, "OPENSSL_VERSION", "n/a"), + cryptography=cryptography.__version__, + cffi=cffi.__version__, + python=sys.executable, + python_version=sys.version, + platform=sys.platform, + sys_path=sys.path, +) + + +if __name__ == "__main__": + print(_env_info) diff --git a/src/OpenSSL/py.typed b/src/OpenSSL/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/OpenSSL/rand.py b/src/OpenSSL/rand.py new file mode 100644 index 000000000..e57425f33 --- /dev/null +++ b/src/OpenSSL/rand.py @@ -0,0 +1,50 @@ +""" +PRNG management routines, thin wrappers. +""" + +from __future__ import annotations + +import warnings + +from OpenSSL._util import lib as _lib + +warnings.warn( + "OpenSSL.rand is deprecated - you should use os.urandom instead", + DeprecationWarning, + stacklevel=3, +) + + +def add(buffer: bytes, entropy: int) -> None: + """ + Mix bytes from *string* into the PRNG state. + + The *entropy* argument is (the lower bound of) an estimate of how much + randomness is contained in *string*, measured in bytes. + + For more information, see e.g. :rfc:`1750`. + + This function is only relevant if you are forking Python processes and + need to reseed the CSPRNG after fork. + + :param buffer: Buffer with random data. + :param entropy: The entropy (in bytes) measurement of the buffer. + + :return: :obj:`None` + """ + if not isinstance(buffer, bytes): + raise TypeError("buffer must be a byte string") + + if not isinstance(entropy, int): + raise TypeError("entropy must be an integer") + + _lib.RAND_add(buffer, len(buffer), entropy) + + +def status() -> int: + """ + Check whether the PRNG has been seeded with enough data. + + :return: 1 if the PRNG is seeded enough, 0 otherwise. + """ + return _lib.RAND_status() diff --git a/src/OpenSSL/version.py b/src/OpenSSL/version.py new file mode 100644 index 000000000..43aea617a --- /dev/null +++ b/src/OpenSSL/version.py @@ -0,0 +1,28 @@ +# Copyright (C) AB Strakt +# Copyright (C) Jean-Paul Calderone +# See LICENSE for details. + +""" +pyOpenSSL - A simple wrapper around the OpenSSL library +""" + +__all__ = [ + "__author__", + "__copyright__", + "__email__", + "__license__", + "__summary__", + "__title__", + "__uri__", + "__version__", +] + +__version__ = "26.3.0" + +__title__ = "pyOpenSSL" +__uri__ = "https://pyopenssl.org/" +__summary__ = "Python wrapper module around the OpenSSL library" +__author__ = "The pyOpenSSL developers" +__email__ = "cryptography-dev@python.org" +__license__ = "Apache License, Version 2.0" +__copyright__ = f"Copyright 2001-2026 {__author__}" diff --git a/OpenSSL/test/__init__.py b/tests/__init__.py similarity index 100% rename from OpenSSL/test/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..b8a263cb3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,33 @@ +# Copyright (c) The pyOpenSSL developers +# See LICENSE for details. + +import pathlib +from tempfile import mktemp + +import pytest + +from OpenSSL.SSL import OPENSSL_VERSION, SSLeay_version + +is_awslc = b"AWS-LC" in SSLeay_version(OPENSSL_VERSION) + + +def pytest_report_header(config: pytest.Config) -> str: + import cryptography + + import OpenSSL.SSL + + return ( + f"OpenSSL: " + f"{OpenSSL.SSL.SSLeay_version(OpenSSL.SSL.SSLEAY_VERSION)!r}\n" + f"cryptography: {cryptography.__version__}" + ) + + +@pytest.fixture +def tmpfile(tmp_path: pathlib.Path) -> bytes: + """ + Return UTF-8-encoded bytes of a path to a tmp file. + + The file will be cleaned up after the test run. + """ + return mktemp(dir=tmp_path).encode("utf-8") diff --git a/tests/test_crypto.py b/tests/test_crypto.py new file mode 100644 index 000000000..61a0775cc --- /dev/null +++ b/tests/test_crypto.py @@ -0,0 +1,3160 @@ +# Copyright (c) Jean-Paul Calderone +# See LICENSE file for details. + +""" +Unit tests for :py:mod:`OpenSSL.crypto`. +""" + +from __future__ import annotations + +import base64 +import pathlib +import sys +import typing +from datetime import datetime, timedelta, timezone +from subprocess import PIPE, Popen + +import pytest +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ( + dsa, + ec, + ed448, + ed25519, + rsa, +) + +from OpenSSL._util import ffi as _ffi +from OpenSSL._util import lib as _lib +from OpenSSL.crypto import ( + FILETYPE_ASN1, + FILETYPE_PEM, + FILETYPE_TEXT, + TYPE_DSA, + TYPE_RSA, + X509, + Error, + PKey, + X509Name, + X509Store, + X509StoreContext, + X509StoreContextError, + X509StoreFlags, + _EllipticCurve, + _Key, + _PrivateKey, + dump_certificate, + dump_privatekey, + dump_publickey, + get_elliptic_curve, + get_elliptic_curves, + load_certificate, + load_privatekey, + load_publickey, +) + +from . import conftest +from .util import ( + NON_ASCII, +) + + +def normalize_privatekey_pem(pem: bytes) -> bytes: + return dump_privatekey(FILETYPE_PEM, load_privatekey(FILETYPE_PEM, pem)) + + +def utcnow() -> datetime: + return datetime.now(timezone.utc).replace(tzinfo=None) + + +GOOD_CIPHER = "aes-256-cbc" +BAD_CIPHER = "zippers" + +GOOD_DIGEST = "SHA256" +BAD_DIGEST = "monkeys" + +old_root_cert_pem = b"""-----BEGIN CERTIFICATE----- +MIIC7TCCAlagAwIBAgIIPQzE4MbeufQwDQYJKoZIhvcNAQEFBQAwWDELMAkGA1UE +BhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdU +ZXN0aW5nMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0EwIhgPMjAwOTAzMjUxMjM2 +NThaGA8yMDE3MDYxMTEyMzY1OFowWDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklM +MRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdUZXN0aW5nMRgwFgYDVQQDEw9U +ZXN0aW5nIFJvb3QgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPmaQumL +urpE527uSEHdL1pqcDRmWzu+98Y6YHzT/J7KWEamyMCNZ6fRW1JCR782UQ8a07fy +2xXsKy4WdKaxyG8CcatwmXvpvRQ44dSANMihHELpANTdyVp6DCysED6wkQFurHlF +1dshEaJw8b/ypDhmbVIo6Ci1xvCJqivbLFnbAgMBAAGjgbswgbgwHQYDVR0OBBYE +FINVdy1eIfFJDAkk51QJEo3IfgSuMIGIBgNVHSMEgYAwfoAUg1V3LV4h8UkMCSTn +VAkSjch+BK6hXKRaMFgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJJTDEQMA4GA1UE +BxMHQ2hpY2FnbzEQMA4GA1UEChMHVGVzdGluZzEYMBYGA1UEAxMPVGVzdGluZyBS +b290IENBggg9DMTgxt659DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GB +AGGCDazMJGoWNBpc03u6+smc95dEead2KlZXBATOdFT1VesY3+nUOqZhEhTGlDMi +hkgaZnzoIq/Uamidegk4hirsCT/R+6vsKAAxNTcBjUeZjlykCJWy5ojShGftXIKY +w/njVbKMXrvc83qmTdGl3TAM0fxQIpqgcglFLveEBgzn +-----END CERTIFICATE----- +""" + +root_cert_pem = b"""-----BEGIN CERTIFICATE----- +MIIE7jCCA1agAwIBAgIIPQzE4MbeufQwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UE +BhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdU +ZXN0aW5nMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0EwHhcNMjAwODAyMTcxMTE5 +WhcNNDcxMjIwMTcxMTE5WjBYMQswCQYDVQQGEwJVUzELMAkGA1UECBMCSUwxEDAO +BgNVBAcTB0NoaWNhZ28xEDAOBgNVBAoTB1Rlc3RpbmcxGDAWBgNVBAMTD1Rlc3Rp +bmcgUm9vdCBDQTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBALpY5jb+ +S7AUbx9gzN06wkqUeb+eNLTjCOKofiMTn8Y0TqCA2ZyY3XMcNBMaIS7hdFTgmmqt +fFntYobxLAl/twfbz9AnRaVDh2HyUvHvMBxKn1HSDLALLtqdF0pcXIjP04S7NKPQ +Umgkv2H0KwcUpYlgjTFtXRiP+7wDSiQeP1YVSriEoE0TXK14F8np6ZKK0oQ+u16d +Wn3MGQwFzS+Ipgoz0jbi5D2KzmK2dzHdxY8M2Dktkz/W3DUfUwaTohYed2DG39LP +NUFOxekgXdIZ3vQbDfsEQt27TUzOztbo/BqK7YkRLzzOQFz+dKAxH6Hy6Bu9op7e +DWS9TfD/+UmDxr3IeoLMpmUBKxmzTC4qpej+W1UuCE12dMo4LoadlkG+/l1oABqd +Ucf45WgaFk3xpyEuGnDxjs6rqYPoEapIichxN2fgN+jkgH9ed44r0yOoVeG2pmwD +YFCCxzkmiuzLADlfM1LUzqUNKVFcOakD3iujHEalnDIJsc/znYsqaRvCkQIDAQAB +o4G7MIG4MB0GA1UdDgQWBBSDVXctXiHxSQwJJOdUCRKNyH4ErjCBiAYDVR0jBIGA +MH6AFINVdy1eIfFJDAkk51QJEo3IfgSuoVykWjBYMQswCQYDVQQGEwJVUzELMAkG +A1UECBMCSUwxEDAOBgNVBAcTB0NoaWNhZ28xEDAOBgNVBAoTB1Rlc3RpbmcxGDAW +BgNVBAMTD1Rlc3RpbmcgUm9vdCBDQYIIPQzE4MbeufQwDAYDVR0TBAUwAwEB/zAN +BgkqhkiG9w0BAQsFAAOCAYEAFIMFxLHaVDY/nsbYzI7+zxe4GJeUqRIj2g4XK/nF +6lHLRFL2YP5yJ+Jm4JDkoZqKq/tcEQLIssQS++s6tBSdvFwdY6imfpEZgFPuodrZ +KbYm4Xuouw09EQCEjPxBOQ1NEcPuwuDtvD6/BOfm3SRFRTq/gQwxKlZ7C/4l8b1+ +OQPIUryqdlFBpyE/M95GzaNdmkQx41PevEih2nqWnbTsXLeiSXLGoubMTxKEK4T+ +J7Ci2KTRJ3SYMgTNU6MNcl7b9Tpw9/KVG80IbpzNQ1LDh3ZtkOfqoou1lmBTeNPu +g2C/oiW6lVAmZx1TL9gbUtkJ0Q2iW4D9TF+zuYi2qpbVU3RvoqK25x3AuIWf4JOL +3cTNjJ/3zmGSORRJvcGyvVnL30R+vwpaxvyuqMjz3kBjkK2Z2pvElZMJiZhbGG7k +MHZQ5A26v0/iQVno6FRv3cQb9EeAZtNHcIEgsNhPZ53XVnwZ58ByvATMLKNN8dWF +Q+8Bbr7QFxeWvQfHYX2yaQZ/ +-----END CERTIFICATE----- +""" + +root_key_pem = b"""-----BEGIN RSA PRIVATE KEY----- +MIIG5AIBAAKCAYEAuljmNv5LsBRvH2DM3TrCSpR5v540tOMI4qh+IxOfxjROoIDZ +nJjdcxw0ExohLuF0VOCaaq18We1ihvEsCX+3B9vP0CdFpUOHYfJS8e8wHEqfUdIM +sAsu2p0XSlxciM/ThLs0o9BSaCS/YfQrBxSliWCNMW1dGI/7vANKJB4/VhVKuISg +TRNcrXgXyenpkorShD67Xp1afcwZDAXNL4imCjPSNuLkPYrOYrZ3Md3FjwzYOS2T +P9bcNR9TBpOiFh53YMbf0s81QU7F6SBd0hne9BsN+wRC3btNTM7O1uj8GortiREv +PM5AXP50oDEfofLoG72int4NZL1N8P/5SYPGvch6gsymZQErGbNMLiql6P5bVS4I +TXZ0yjguhp2WQb7+XWgAGp1Rx/jlaBoWTfGnIS4acPGOzqupg+gRqkiJyHE3Z+A3 +6OSAf153jivTI6hV4bambANgUILHOSaK7MsAOV8zUtTOpQ0pUVw5qQPeK6McRqWc +Mgmxz/OdiyppG8KRAgMBAAECggGAGi6Tafagu8SjOE1pe0veMIxb7shTr3aWsQHr +dxIyyK5gvbxc1tvDgYDc8DIjp2qV5bcI+yQU7K2lwj/waAVBuiDwOdbKukWap/Bc +JxHsOI1jhSN2FOX9V0nrE8+WUMKifWuwIbQLYAaJvUGJKh2EhKDENcWf5uuT+v6b +VCfLzlR/gx1fSHUH+Hd/ICd1YdmPanVF7i09oZ8jhcTq51rTuWs+heerGdp+1O++ +H4uBTnAHkUEOB1Iw7mXQTIRBqcntzob/TJrDKycdbFHEeRR0L1hALGEVftq7zI6F +BA9caO1W7HkcVmeT6HATIEIGG5H7QAwSfZflJ/82ZXtDemqhBRVwQ2Fx/99wW3r9 +puUvJyLbba7NCwL1+P9w8ebr00kFyYoy6rE1JjqlE+9ZHwakZUWTA1lMOGWNEkRS +bKZNHgrngs2zk5qCYRllmsBZ3obdufnP/wyag+BFVniAIN3a08y46SYmgYTeLdBX +/DHSZIKWI9rBiNg6Qw49N+06XwiBAoHBAOMZQbRT8hEffRFbxcsRdJ4dUCM1RAXV +/IMLeVQgKEWETX3pCydpQ2v65fJPACfLLwwRMq4BX4YpJVHCk6BZh/2zx8T1spmJ +uBkHH6+VYgB9JVU0hv/APAjTZxdBjdhkaXVxccpmBBJqKKwOGf3nRVhmMsItBx2x +ZCz+x50+buRMTKsF+FeK2Dr2e9WrfMkOJ3nQFwbGvOBIQeXKmu0wYUVyebnCdZW5 +pKI0Co7wp9soCa02YvTFR8n2kxMe9Y91jQKBwQDSD/xSsRfgDT0uiEwazVQ2D/42 +96U2MYe+k+p1GHBnjIX4eRPcWOnQNUd/QVy1UK4bQg1dVZi+NQJ1YS3mKNCpqOaK +ovrgHHmYC1YIn8Xmq2YGzrm/JLwXw0BkPhHp/1yQVPVgyFKeNa3fSa0tkqCed5rs +erM8090IIzWPzKtXId8Db4i0xHkDzP7xDThb6pPNx5bvAaempJRDLtN9xP/hQRyh +xZ/MECKGRgyAVfndIZaI82kuUQFlnPMqk4FxFhUCgcAhnMdgzVvytNpqC09HMxoz +nNsTmvqqcnWhX71hejD7uQ1PKYMBHk9gWA5YwuCfAy+/dXwuzP06ejSP2WDIRvgd +0NIskMESgJPDAI7sCgwrTlqMNe4VRHqeQ8vqYUWBVbtWKqhQ8LCBmTzT2nJ2ZhiZ +cObqXofDGVJeZodc+rSnDbP7TDLpoh9G+txxT6R0jafCG86MrjWebJN0U3yCxrpe +8QabO/DzbDq110YIyg3OHirwfDBBUkHB3sD9/4MQ7LECgcEAs2UFhxVIn4aO5ott ++0G5lkYIQ6cwx9x64i3ugDvz2uruiunUJU0luTOXML2AUDRrzEmXokr0nBQnWlk4 +2qOmuA3PfTx85iJLUab0vX69gyaDhnLLvMrBe8W62yELKXx076ouuI27yPNs3xFL +vWzIkSzx+N0870i8LjPrjTgsZ8g8bfG1nTNhafaLDw/MPutReN7oLouKQs2w9MMr +yPAR2qxBqIJe2uY4pdVy3bMPJWOG7MR74hs6By6HmKfKVuqVAoHBAMRSefX1QtfS +3wWpQhkE7Sooco4LI8kfNncZ2gzNDbYf6aOkgzv0/SWJh+CdcKep9xk12O02Lpsm +SsPYeYlPDCCvyJYGpR19QocYp6JCaemb7uMd6FuPHSHUgyoR4GS8PUuIbiRnpPxN +4ta7VzmIZOCFu5e+vOq1NwTd0hR6sy5uNsTHV5ezOOqz2SB+yTRMDPr7cW0dMSJ8 +jsvxvqVnkIhWeuP9GIb6XUhq74huGZ0Hpaxe6xG34QYiBpr/O3O/ew== +-----END RSA PRIVATE KEY----- +""" + +root_key_der = base64.b64decode( + """ +MIIG5AIBAAKCAYEAuljmNv5LsBRvH2DM3TrCSpR5v540tOMI4qh+IxOfxjROoIDZ +nJjdcxw0ExohLuF0VOCaaq18We1ihvEsCX+3B9vP0CdFpUOHYfJS8e8wHEqfUdIM +sAsu2p0XSlxciM/ThLs0o9BSaCS/YfQrBxSliWCNMW1dGI/7vANKJB4/VhVKuISg +TRNcrXgXyenpkorShD67Xp1afcwZDAXNL4imCjPSNuLkPYrOYrZ3Md3FjwzYOS2T +P9bcNR9TBpOiFh53YMbf0s81QU7F6SBd0hne9BsN+wRC3btNTM7O1uj8GortiREv +PM5AXP50oDEfofLoG72int4NZL1N8P/5SYPGvch6gsymZQErGbNMLiql6P5bVS4I +TXZ0yjguhp2WQb7+XWgAGp1Rx/jlaBoWTfGnIS4acPGOzqupg+gRqkiJyHE3Z+A3 +6OSAf153jivTI6hV4bambANgUILHOSaK7MsAOV8zUtTOpQ0pUVw5qQPeK6McRqWc +Mgmxz/OdiyppG8KRAgMBAAECggGAGi6Tafagu8SjOE1pe0veMIxb7shTr3aWsQHr +dxIyyK5gvbxc1tvDgYDc8DIjp2qV5bcI+yQU7K2lwj/waAVBuiDwOdbKukWap/Bc +JxHsOI1jhSN2FOX9V0nrE8+WUMKifWuwIbQLYAaJvUGJKh2EhKDENcWf5uuT+v6b +VCfLzlR/gx1fSHUH+Hd/ICd1YdmPanVF7i09oZ8jhcTq51rTuWs+heerGdp+1O++ +H4uBTnAHkUEOB1Iw7mXQTIRBqcntzob/TJrDKycdbFHEeRR0L1hALGEVftq7zI6F +BA9caO1W7HkcVmeT6HATIEIGG5H7QAwSfZflJ/82ZXtDemqhBRVwQ2Fx/99wW3r9 +puUvJyLbba7NCwL1+P9w8ebr00kFyYoy6rE1JjqlE+9ZHwakZUWTA1lMOGWNEkRS +bKZNHgrngs2zk5qCYRllmsBZ3obdufnP/wyag+BFVniAIN3a08y46SYmgYTeLdBX +/DHSZIKWI9rBiNg6Qw49N+06XwiBAoHBAOMZQbRT8hEffRFbxcsRdJ4dUCM1RAXV +/IMLeVQgKEWETX3pCydpQ2v65fJPACfLLwwRMq4BX4YpJVHCk6BZh/2zx8T1spmJ +uBkHH6+VYgB9JVU0hv/APAjTZxdBjdhkaXVxccpmBBJqKKwOGf3nRVhmMsItBx2x +ZCz+x50+buRMTKsF+FeK2Dr2e9WrfMkOJ3nQFwbGvOBIQeXKmu0wYUVyebnCdZW5 +pKI0Co7wp9soCa02YvTFR8n2kxMe9Y91jQKBwQDSD/xSsRfgDT0uiEwazVQ2D/42 +96U2MYe+k+p1GHBnjIX4eRPcWOnQNUd/QVy1UK4bQg1dVZi+NQJ1YS3mKNCpqOaK +ovrgHHmYC1YIn8Xmq2YGzrm/JLwXw0BkPhHp/1yQVPVgyFKeNa3fSa0tkqCed5rs +erM8090IIzWPzKtXId8Db4i0xHkDzP7xDThb6pPNx5bvAaempJRDLtN9xP/hQRyh +xZ/MECKGRgyAVfndIZaI82kuUQFlnPMqk4FxFhUCgcAhnMdgzVvytNpqC09HMxoz +nNsTmvqqcnWhX71hejD7uQ1PKYMBHk9gWA5YwuCfAy+/dXwuzP06ejSP2WDIRvgd +0NIskMESgJPDAI7sCgwrTlqMNe4VRHqeQ8vqYUWBVbtWKqhQ8LCBmTzT2nJ2ZhiZ +cObqXofDGVJeZodc+rSnDbP7TDLpoh9G+txxT6R0jafCG86MrjWebJN0U3yCxrpe +8QabO/DzbDq110YIyg3OHirwfDBBUkHB3sD9/4MQ7LECgcEAs2UFhxVIn4aO5ott ++0G5lkYIQ6cwx9x64i3ugDvz2uruiunUJU0luTOXML2AUDRrzEmXokr0nBQnWlk4 +2qOmuA3PfTx85iJLUab0vX69gyaDhnLLvMrBe8W62yELKXx076ouuI27yPNs3xFL +vWzIkSzx+N0870i8LjPrjTgsZ8g8bfG1nTNhafaLDw/MPutReN7oLouKQs2w9MMr +yPAR2qxBqIJe2uY4pdVy3bMPJWOG7MR74hs6By6HmKfKVuqVAoHBAMRSefX1QtfS +3wWpQhkE7Sooco4LI8kfNncZ2gzNDbYf6aOkgzv0/SWJh+CdcKep9xk12O02Lpsm +SsPYeYlPDCCvyJYGpR19QocYp6JCaemb7uMd6FuPHSHUgyoR4GS8PUuIbiRnpPxN +4ta7VzmIZOCFu5e+vOq1NwTd0hR6sy5uNsTHV5ezOOqz2SB+yTRMDPr7cW0dMSJ8 +jsvxvqVnkIhWeuP9GIb6XUhq74huGZ0Hpaxe6xG34QYiBpr/O3O/ew==' +""" +) + +normalized_root_key_pem = normalize_privatekey_pem(root_key_pem) + +intermediate_cert_pem = b"""-----BEGIN CERTIFICATE----- +MIIEXDCCAsSgAwIBAgIRAMPzhm6//0Y/g2pmnHR2C4cwDQYJKoZIhvcNAQELBQAw +WDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQHEwdDaGljYWdvMRAw +DgYDVQQKEwdUZXN0aW5nMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0EwHhcNMjAw +ODAyMTcxMTIwWhcNNDcxMjIwMTcxMTIwWjBmMRUwEwYDVQQDEwxpbnRlcm1lZGlh +dGUxDDAKBgNVBAoTA29yZzERMA8GA1UECxMIb3JnLXVuaXQxCzAJBgNVBAYTAlVT +MQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU2FuIERpZWdvMIIBojANBgkqhkiG9w0B +AQEFAAOCAY8AMIIBigKCAYEAo3rOxOVrdLdRsR1o0+JG7MRpCtMoafA63TM/DczL +Q4jURv5MzyTE7FFdXq4xNJRYgD16vUavZluQGj30+5Lkt07CuO/BK3itl8UW+dsH +p95gzBvgnj5AVZGkNOQ0Y4CbXO087Ywep7tpBfZ5fzURLeH+OHQGseEFZ5e0w8Az +AarWu+Ez5RGpkaZ61iiJa53mAgkrjw+o83UrpDT2nrXiyR6Fx4K4eb1rarodWqGv +jSkdT5MA4i0gDhsIBnTarPB+0KM8M7od8DkLsTHBt4rYYCHgCX1bWavzGlqPEw9h +ksK+LAbQKD9J2AxYDkL0PAeUuvWMhxEmN6hXePiw63sJzukRunAvut5A2+42JMkW +guDyqIvAjlCYcIyBvUbphP3qSFqww/hpZ2wh5UZOc1mzYJKR9MgI8/UhRJEJ7NyY +pF24EJbisjNE30ot8aM2/5cI5KevclcuPJWH8PjT/i1VnNpM4S8MqoPw6F+d75d/ +CtfI+LLfns4k3G9I+Qgxmpa5AgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQELBQADggGBAFVQ3Dmljrnbjys9ZIqcTs/B5ktKUAU2KNMO9TmoFymE +YhHKbCb5u/CnWq3jtBW6jgkQHrhfY9leUlH87BkB2o16BcSKjHknHZ2MCdEvQvOM +/nkkMDkOEoRn8mfCCxxgt8Kxf07wHDcnKoeJ3h9BXIl6nyJqJAcVWEJm1d75ayDG +0Kr0z+LcqMtQqYI0csK/XDQkunlE95qti1HzxW+JeAf6nRkr7RNZLtGmUGAMfyBK +9A0Db8QLR7O92YEmwoXtp+euN6uDdjw4A7KHjNXMdvqZoRfbZEA9c6XJTBj22h87 +gYUFRVpkNDrC/c9u6WgA943yMgFCwjrlTsmi+uoweT9U5r4TA+dVCDAv943aWCNm +A+TiuIXlJAHl2PlH7Umu/oMQKDEt+0n4QcQLBZyK3CYU5kg+ms9vOvE19Lhp8HeS +xqm6dwKpdm7/8EfGNW3s8Gm4KM26mb7dtSdHJFuR/BQ5y/cn4qIMyeGfHvsVew+2 +neyFR2Oc/nUlZMKfyHI+pA== +-----END CERTIFICATE----- +""" + +intermediate_key_pem = b"""-----BEGIN RSA PRIVATE KEY----- +MIIG4gIBAAKCAYEAo3rOxOVrdLdRsR1o0+JG7MRpCtMoafA63TM/DczLQ4jURv5M +zyTE7FFdXq4xNJRYgD16vUavZluQGj30+5Lkt07CuO/BK3itl8UW+dsHp95gzBvg +nj5AVZGkNOQ0Y4CbXO087Ywep7tpBfZ5fzURLeH+OHQGseEFZ5e0w8AzAarWu+Ez +5RGpkaZ61iiJa53mAgkrjw+o83UrpDT2nrXiyR6Fx4K4eb1rarodWqGvjSkdT5MA +4i0gDhsIBnTarPB+0KM8M7od8DkLsTHBt4rYYCHgCX1bWavzGlqPEw9hksK+LAbQ +KD9J2AxYDkL0PAeUuvWMhxEmN6hXePiw63sJzukRunAvut5A2+42JMkWguDyqIvA +jlCYcIyBvUbphP3qSFqww/hpZ2wh5UZOc1mzYJKR9MgI8/UhRJEJ7NyYpF24EJbi +sjNE30ot8aM2/5cI5KevclcuPJWH8PjT/i1VnNpM4S8MqoPw6F+d75d/CtfI+LLf +ns4k3G9I+Qgxmpa5AgMBAAECggGAc0i/V4qR5JUCPuyGaCVB7uXzTXbrIQoP+L2S +0aCCFvX+/LGIaOt9E0mtln8wo+uZHZY9YAzg1EXtsRPQFzjXoY0hNFme15EamdSb +B0e2dmMTz9w44l7z72PtcH8dkq224ilKthoB5Db9MP9HXrWFj9228QihT/9nWE5b +Y0++qIZZN9TwS7HQ6q2EIlIj1ohbE0R0O0bH1ifixsGyyOlrLHkhzjgY74Dspy7o +VGmA6wL7cIoyLU21NT1Kw4LUUvCk3MTd62gIg43qLsoLJ1AVZg9AmLmhZn4HiGZa +tiE1+Iz70E+qxIXDQTip/EY4qe9HHYM2VccjlMQsLLCw5Y2CJL0xbRUSPkKev+Us +PyasHgxPP6s5sHTKm0fee+riJrR+CqODGT45CirJr+WjDznlJETgVDW5DmtTWGVW +2WeBarXdYOn4S+uK3Pe3aTAiE9Uw7KrOdJqrWg89YFnMWw4HlMz0369HCUv5BqSg +qtrJ7iPJhN5MMhA4Te2Rnc5onqEhAoHBANKmZP4/g5RoYy6Gjkwe9PSgp9URxCJt +VHiE5r33jXxOMw2lJQD8JVLmWuNTbKEClj6Rd/5OzM2q2icYDu0k/wcX+BgXg5b2 +ozyfjzgnqddKs8SlNd9oc2xiFRLgBkdHI5XFQlcp6vpEM+m47azEw72RtsKObN0g +PZwSK8RWTj4zCXTdYMdr+gbdOA3fzUztckHLJQeS42JT3XJVSrSzFyXuVgXmdnS9 +bQ2dUfPT+JzwHy/HMmaBDM7fodDgv/XUywKBwQDGrLTomybbfc3ilZv+CZMW7bTy +pX8ydj6GSIBWLd+7gduQHYqam5gNK2v4BKPVHXMMcRZNIIId3FZztMaP3vkWQXIG +/bNBnL4Aa8mZFUle1VGoPZxMt1aaVLv3UqWi47ptciA6uZCuc/6si3THTsNr/7kR +k6A7UmA0CRYWzuezRsbEGRXZCCFGwJm2WCfewjNRqH/I+Kvfj06AddKkwByujfc6 +zQDH/m0QFNAKgEZYvFOL/Yd2cuFhU2OPUO4jFgsCgcBXRbjx3T6WbekpjXXG88xo +zWa7T/ECkmk8xVMTwUxNA9kC/jimf9C219kv9ZA75OZ6ZaphIiSX0QEw0Tbd6UX/ +ml6fHJ7YHLbklvavPT+QgtKX1hrLxGqNrNUuTMJNJZwIoQErO6KurTMU0hkmSx8N +myEs2fUgaAsebijT3y3rdxmj4VQHSyT7Uwu2M9LK3FVKDO/6g1DRnA1TISMiWlBs +1qGtMB5Dn3de/J7Hdjq6SoGhOdYXwb+ctepEr9jX8KECgcAE2nk86XVkjUk3TNJX +vWIjgEEYYGSgFfVnEGRaNpqtmPmFJsOZDU4EnFfx4iMidKq31hdmYPHsytIt12+2 +WgsZuRWRCCeV5b9agUeWfsehEnMBOigUU7JA6OsCmrlDJm8Kd2xEIv5e1KSXEH0U +1V6+x6t8u2+Bo3yIKOSqP/m3DnaSmc5H1AQEF3Zp1vN6ZKIeT5B3l2OTfYu8ZaR0 +s+C/fuZYQGPRfuypJOkEKKgPSOJ9m/7wLNRGrWPUP3Th1IsCgcBb2O9ROv793a3x +PtW4qzkqF69KKc2O/vT819NBQjGopQetOcsY3VHp0eJMv85ut4cCeqScAfdtFIiC +ScnrBO4JtdE6FkTY1k8el1DrctrUR3PZ2rt3m5k2XfPDGEypH3BReD3dHUe2M99D ++dceH46rKyMXQ2lLA3iyzGE6NyWUTZ6co35/Qm2n8lV9IG1CuX5HVAVrr2osLG93 +zZvFSeTrN2MZvmelhS6aUJCV/PxiQPHlou8vLU6zzfPMSERTjOI= +-----END RSA PRIVATE KEY----- +""" + +server_cert_pem = b"""-----BEGIN CERTIFICATE----- +MIIEKTCCApGgAwIBAgIJAJn/HpR21r/8MA0GCSqGSIb3DQEBCwUAMFgxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJJTDEQMA4GA1UEBwwHQ2hpY2FnbzEQMA4GA1UECgwH +VGVzdGluZzEYMBYGA1UEAwwPVGVzdGluZyBSb290IENBMB4XDTIwMDgwMjE3MTEy +MFoXDTQ3MTIyMDE3MTEyMFowGDEWMBQGA1UEAwwNbG92ZWx5IHNlcnZlcjCCAaIw +DQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKU9txhKg6Nc0dVK9Vv4MYuYP6Hs +oR483+wC53V8axkfy2TynrBSug8HapeSFW5jwdwcsjaDwEIAugZfRoz0N1vR/Q6T +OFAYn2hRwlAgUXVk3NXpDNV/QRliGvxhLAVpvu1a4ExfVZoOQyPa8pogDgrUdB3e +tYmmFHNa09Lv1nyMZWi6t7zH2weq6/Dxpm0BWf+THFcunv9TNfAqmDV5qbxvaUPh +uvRpN+X2N3tejB8WKt+UmzAXUi3P3OgYimWXwq8Rmorc1rk5j+ksl6qYwZvi7rRV +g1ZAH7bGhXC9eEU/1Z9q26OhAPdTyJD0pc+G9vMz6VijLRXcgHBUP09lSeqxnNxc +pWoX6nRdGn6PkDhewHM05iqAE3ZHnc8kSBcRX85SoW5dGOhvvUTs4ePVNTo3vHdQ +vftTDD+I3rbFnYTKUAzHTPSWGE7LVEiWJ94RKSADXgve0qq8o377UMnY7W3UygSY +odyUZ29B5EfZ88EpIs/h5NomDv5VcQEoCWN1owIDAQABozYwNDAdBgNVHQ4EFgQU +g1V3LV4h8UkMCSTnVAkSjch+BK4wEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZI +hvcNAQELBQADggGBACn0LsqO94tk8i+RbG5hryNduem9n8b8doYD97iaux6QLvY/ +A8DFduJtUevZ3OCsRYQSGa3V/ysMzN7/DIUkpRLevZmdw+1L6PGR7peR2xIQ+yEW +bL88vLjezaYIzMKHJRmN8oP3DQtGJm6U2fMMiEHWqRtULIVpnFppzPI2z7+tDeyg +PFD2YeiFWoq5lmXStrK+KYPJbhTn0gz4QlGBs7PLY2JMDRSVj6ctkvrpXbC3Rb3m +qo2FY/y51ACg77Txc6NAmNE6tCknwaUjRQP2MuoYFm5/Z6O9/g49AEVIE101zHqV +N6SkcTUaXAuQIyZaqwdndfOB4rrFyAkoxTM5OamIQl80hZKf4R5rM7D7Sz8kAWJi +BPIcewN0XnI6lm+zPAVUAE8dZfgJmJR5ifZHYCuv96EX0RpYsddeik8UmjkZ2/ch +vRzvRSNNxVC6Zoe6vKNUb89XMtJZqY80WxfWG3Z2Hwf9KvS+2KAH/6MiSMj0RI5F +SCB2PMQm6DYXwM1EyA== +-----END CERTIFICATE----- +""" + +server_key_pem = normalize_privatekey_pem( + b"""-----BEGIN RSA PRIVATE KEY----- +MIIG5AIBAAKCAYEApT23GEqDo1zR1Ur1W/gxi5g/oeyhHjzf7ALndXxrGR/LZPKe +sFK6Dwdql5IVbmPB3ByyNoPAQgC6Bl9GjPQ3W9H9DpM4UBifaFHCUCBRdWTc1ekM +1X9BGWIa/GEsBWm+7VrgTF9Vmg5DI9rymiAOCtR0Hd61iaYUc1rT0u/WfIxlaLq3 +vMfbB6rr8PGmbQFZ/5McVy6e/1M18CqYNXmpvG9pQ+G69Gk35fY3e16MHxYq35Sb +MBdSLc/c6BiKZZfCrxGaitzWuTmP6SyXqpjBm+LutFWDVkAftsaFcL14RT/Vn2rb +o6EA91PIkPSlz4b28zPpWKMtFdyAcFQ/T2VJ6rGc3FylahfqdF0afo+QOF7AczTm +KoATdkedzyRIFxFfzlKhbl0Y6G+9ROzh49U1Oje8d1C9+1MMP4jetsWdhMpQDMdM +9JYYTstUSJYn3hEpIANeC97SqryjfvtQydjtbdTKBJih3JRnb0HkR9nzwSkiz+Hk +2iYO/lVxASgJY3WjAgMBAAECggGAJST2X5OAe9yFnri25vGn0YVr6G5U2YM9osQU +W6iYOpGXGx4e5evyvyYfo+rGvoXWMjCRLwf2099t8bjBFzZeq1lM1VXqtraSPtUC +JRjettDxg3Rb2jI85APVpR4C00SuEpT3DrPvfi3ukcTJ/DNwdKbFY2GI1WRr/HJS +Y3xebqjwstYmL12Nsu+NEiCAFMjU/kqHeGGWhDakTVSF2p96tE0nEIdRi1eLpTnv +xt++B87n3FJ/gBP9+SZcth+uHKA8Wr42CqJR3z8b/blICYCd2LABFdZjL4aHfce9 +Xe7UyVoySYC6N0YSbLLfsVu/w/qsYitcTvWCyekX4eT2U9Sdje46LGN4MFJSYy8K +Qw4hzz6JhUrAiwxPb2MLkq6q7AvdFwVAFl7xuH9J13yuN9x+w4NL9h3hzr4iC7nk +xVrmme279h1hfuCR1+1Bb0fLvdl5VevT9SZYCg5BCL7JxHGofcBZ3ZE9R9Q7QYVv +rCKHFZ5tIOkVJk2mcR5NvK6r7ethAoHBAM7BFvBPHgJ5xtny7M9xvaMQD9PZ3zzb +PUD83lh+DlmLyzKLw2/OblyJgO8ECWUDNR1QkL5khq5Z2t1Kj77Hak7mUUlICbIc +LKZLiAosuKBo/ps6emRRhIf9NNYR2G1k9GWyk3KicD/htllPl10j64vgBg2M/LQJ +2Oh95oWMck7RRdWHCwfBjND3YsYoN0hY9GXgr+ByDRQgAacvnpHlFCRmSPqiAJGh +kPKIRfjLgVFbL1cIj7oHpcModgZr7Dgc/wKBwQDMmVhsmiefTscZSCoCIqXVsJJ0 +edDmIvAl3cFozf9/+5JADjnp/9zcdANNN/oMfynOPx+0R2CygxooZaRKbnHPcVlu +SCxwaloagNSFVt8lZ2PwybutfdMN8YbU431ypNLJjInI3Z66eHBRDZZZviu5AtoL +5WYAvFzN502P1IVrJBo0lht7ftQMwM4NAhRaaFrUCrycREwUl0u9PxswmDhignWs ++fyJ93D5aVC1wHjUN9WYTEOM66goZTuSDD8mE10CgcAbl3UiOMy+c9XvvBWSUZGH +M1uJYCgEjRWNmLFridcMaDWD11cLkrbzrn4AZ7+BNX5fHSNT5UJ7/g3RPmQUh7RO +Nzpd1zlEBbKHtsi+4tz4u0pPGOzAeoh/RXFJqDQD1VcwQzaeM8NbIxocrRx8F5EV +p53nLQuEU1QZIsQiym1uy0rQhicYr+HE+V67Jx7JjuV+uw99mnrYVrUhxJ8axUF8 +4hGXMQt2Y+NeGoWMAEyPuOWGbeQQZXjfpISrsrdhfa0CgcEAxqbdRBUpA3Tpu5Jl +t00M1z5p9M2SFuE1ao61i5z3xrvsdGVbtefH+gRqcD85eYi+fpKrpc7oBGtmqnKF +4f76YgAcZQeOnlekxLbxocWHRDnuv4wfvYO9uHwZ/fojg3ylbSwXXABSbZsi8o/O +u7P5n9k0/Pfu4igBs6oxlMU0BaM4DnbwmCe8m+VYKykpud440kjaeJ+XfyanU0hC +jhw+Iueoehr/KLYn6wJmaxJGP0c3DHh/3gOxcgdYn6VkawPBAoHBAMJ7jfxZJfBO +i0gDsD9Kz3EkGI8HbBpgC2Cd9DGQR9qTZy1/l/ObM2jwNumJjoHsN8fkha1d6/3n +01hA4LwLB/SLQHx+7k1749sH7m1FaczWa9eUxNkwFiVTBYIyvbekNfJogLX9pVow +vEuNe+J8vxLt3gQJ1DUz+2Air8v//OIqQ+akDnPkwiqHDqynNNWO+jq708aUunVT +TTvknsoT3qT8H/N1FwbCZ14eKV+bXHcv1lVrLdW/DnjDZRpMFa3LSg== +-----END RSA PRIVATE KEY----- +""" +) + +intermediate_server_cert_pem = b"""-----BEGIN CERTIFICATE----- +MIIEXTCCAsWgAwIBAgIRAPQFY9jfskSihdiNSNdt6GswDQYJKoZIhvcNAQELBQAw +ZjEVMBMGA1UEAxMMaW50ZXJtZWRpYXRlMQwwCgYDVQQKEwNvcmcxETAPBgNVBAsT +CG9yZy11bml0MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVNh +biBEaWVnbzAeFw0yMDA4MDIxNzExMjBaFw00NzEyMjAxNzExMjBaMG4xHTAbBgNV +BAMTFGludGVybWVkaWF0ZS1zZXJ2aWNlMQwwCgYDVQQKEwNvcmcxETAPBgNVBAsT +CG9yZy11bml0MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVNh +biBEaWVnbzCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAL3UcTxwCsMZ +qIE+7lolm8t6lT0IYZkE4L7u2qI64m9CvztudqqKYZcrprZobZxqPhqc8IO3CFR2 +nVzwZWxrHCcm6nAzJjVXUFrc4TLsVYYJL1QvKXxr97VIiySU7x6xWrQQsqDtlrb0 +jH59EYFbM2eMk2fBT2X4h6YMXlqyrDjZF6apClXtkdxGJGqR5PCTs4cvrYW7TpIm +cuJq0S+MRBguZpriM+wOK7cXrqfRPFRzZtPXskpQPSAMDDAOGKl8OZfoVFYzG8KG +omOa0hcHtgYX2GCDs1g1maY6Haw9bgs041BoApH9aQxehy5dfU39DcFoKSE3dCjR +FaR6ryCA+f8L1F3xVaHsvX443CYF0/holfsptTjNd1T1z8WR5h1jtY0gJ/ERgcJZ +UgDRE3lEkTLExS/nuGVfdwnlkxny9jbtYp2YcjYjUkChLtTgz4ommeIdBdDvSu8M +wWHMtQNxECs5qA5J384cLh11Nd9exWUjiQ9yAZ0qTOzTkdH7VPHfxQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBgQA2jC5hJ/+46RLBuaZiJhBawiY+HqybEAZWM/IBGZO4 +UKcRotovU+sb1jg+vpXwePSBPEtQoZce0jN0TKiCdlLM4/9lybAvc6qBLJ0d4VS5 +BU5QsCs9IKyvswAFVipQZi0szYwHk8T145SH/fPao8oznf5ae4a6rK9PyZqT7Ix1 +nnKGffbJs0dY+jlxmx/BPlbsGfTwPL6LexghjvbpbXWUdVLP3gAW6DPCtRd6lhWj +JvgCkF2SnbQ7GgnPEYi8h09j0c6/sK6jLoNAatJyIlRGE1cdGYZVUlVW/xP6lYM0 +Mi1KKl0ZXOne4vPTtnTBBqrpjdLydH3WM1IxdwSRbmF15OD6BWzzKV4IYUJ21GDh +YrVrcIeN49pUoKVTTn0Sql8f8mXxJhJ54wo9TKdIGZeuwTZrfWjcjWghXgghXGoP +RI/I5fk/OMu0Oc06/+xdwCBHCSge0/vxK6fhTu7PxmJhQcZF0sDZyb6LXm2feVkG +6FsxnsvstVNO3oJdpa8daLs= +-----END CERTIFICATE----- +""" + +intermediate_server_key_pem = b"""-----BEGIN RSA PRIVATE KEY----- +MIIG5AIBAAKCAYEAvdRxPHAKwxmogT7uWiWby3qVPQhhmQTgvu7aojrib0K/O252 +qophlyumtmhtnGo+Gpzwg7cIVHadXPBlbGscJybqcDMmNVdQWtzhMuxVhgkvVC8p +fGv3tUiLJJTvHrFatBCyoO2WtvSMfn0RgVszZ4yTZ8FPZfiHpgxeWrKsONkXpqkK +Ve2R3EYkapHk8JOzhy+thbtOkiZy4mrRL4xEGC5mmuIz7A4rtxeup9E8VHNm09ey +SlA9IAwMMA4YqXw5l+hUVjMbwoaiY5rSFwe2BhfYYIOzWDWZpjodrD1uCzTjUGgC +kf1pDF6HLl19Tf0NwWgpITd0KNEVpHqvIID5/wvUXfFVoey9fjjcJgXT+GiV+ym1 +OM13VPXPxZHmHWO1jSAn8RGBwllSANETeUSRMsTFL+e4ZV93CeWTGfL2Nu1inZhy +NiNSQKEu1ODPiiaZ4h0F0O9K7wzBYcy1A3EQKzmoDknfzhwuHXU1317FZSOJD3IB +nSpM7NOR0ftU8d/FAgMBAAECggGAYNwla1FALIzLDieuNxE5jXne7GV6Zzm187as +mFqzb1H/gbO7mQlDAn+jcS+Xvlf3mFy73HloJrDfWqzPE6MTmmag+N8gf9ctiS9r +OTCd8uZ839ews2vj2PxLAz97Q437WiWq/7I7VN8zUNdAN2DxucRg8nAQs1c8390v +x9ejSN580u0t+OpfoqWnrzkCOD8lO7V4NOR+EtTLifw3AKvxkuUaNa12ENyqMaJD +3B1HS1AXB8DnmEOY7OE41sxaiSB44M7tsr31ldUCbEf/A5OZWeCfloP2c2g+Td8s ++sl+AzoGa1HsFOqiqdDw8lKynfT1VukaaCtOr0pGeh6XW65aHRGI0B+mHIEM7yR0 +f2NjfvgejqNekWyJ+XeTcmrPPcSH72F9ansLRpUullAi+6OkPFIiwyKCP/S2sLwh +cqe3NITfMweWDt7GqgOhz1yWaewXgdruWiFAYAh2JDBtgMWTUwWgkKyFCb4mrI6r +zqiBpA8Mjm/H17h/dQqF3iRuaZOBAoHBAPDvVseeiGwZjDXuQD9acCBZU23xwevR +6NVe/FLY1bybgsOBQCApQIWKH72eIHo12ULRMe/uZUo3su9JSCc8Gt8DsQpiZ2a+ +z8rS6uEw/UZGMWeLjcIVK5IeeD7OJ/BXEbwoxVvWLYYgWHpYwY9eqppsMlVqmIHY +lfRAaepEkU/4euRl1VTFxkU0sYw7Tj+gbFQDydB0NSLIU/x10tlHblT+O5tgBLJh +kL7II9tyoGaCUjNnACErmi1FA+lNsx1eAwKBwQDJsw+sIhujRHrajCV5dqq5cx3h +ZQNfamoX6xfXYjNHjkeFnFpHB2w6ffe00q2Kt5+0AaSA295n1vPx6IKzKYMr8kpD +0Kiv+mlKK5w7lZzdCeoJb8Co2t9viZXrN9lNetXiSZldrg5nlG8Gmi2RKn65vIfp +ZFc8CExXpQWNMSLJlu2qM8Sjt4h8M880khuTggCeIDbw7YfyanlNhsNpOGv/r+Hd +3i0BP0Qd1sZWkZ+hp/JJFdvyEh5vINgSABfNJJcCgcEA8LqioVcEBcZM8oG3jdVF +3PyDQIHieUXFdpOuVvSyMf3LXJ3ivX+aKRNF/YZl+tWc24b7dzhh2hLm5PD6d8E1 +NAiTNsX1fJJAOe4dopz5IuL1b/jezcGrRBbPnCkNfLTyUmcGMmlAGRhubugJlb9H +hH2AmRmlgW8u/NnzOZADBL1HxLb+vPHS1cj9cRi8aRRXyGX0miPSB4vTZpcu8cvO +MHvIgMkiSDz1i7mbIiNYorOpgBR066+OH5cqfkwVH82TAoHAO3dZdYyQzXARMIIF +QmxkJUz1UFCxz93V7btYSh4ftEcUeyX/z9U2aYBeGafLloxQv4eEcqFgTwkm3vmI +Hz5r9/b1Qk0wjsGrbTyyUTbpCpozsBiMmrv9CCtuUe0jWh6PFKpSVzZL9OnkWfP2 +30fCGQymnX8B4ScpKuXyXxBPi1O+OmIM5Z/k04mK25sAGltHx1cEG8BMRoJxxROo +ZUtHPBkk5H7ukeGPOaTq0PcaM1UKr9WMBTCmXGk4iwYP/mF9AoHBAOTlFVgGbbjk +Cp/Wd7IrYCBKlnkIyBUMx5icLcsFmgXWx+Gx1HualD2aZ7kctYOfo+zLEyA6roni +bSFLrxT4Od4uqwb51iZoJWxO+C3H1i9NoieU5JOnw5Osyl7OMXm3DkaS/N+ipP/b +3bx1y8/WnGgqWWguXKt2lmgOItaEKrXYr6VZ1Z4upnLtkbxLANnqkQcL9287tXaW +GPVXEteEXrtPj1f+9QYsMKuTWfaw6XfnBkBHxEZgWR+2hAN2z3c/Eg== +-----END RSA PRIVATE KEY----- +""" + +client_cert_pem = b"""-----BEGIN CERTIFICATE----- +MIIEJzCCAo+gAwIBAgIJAKxpFI5lODkjMA0GCSqGSIb3DQEBCwUAMFgxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJJTDEQMA4GA1UEBwwHQ2hpY2FnbzEQMA4GA1UECgwH +VGVzdGluZzEYMBYGA1UEAwwPVGVzdGluZyBSb290IENBMB4XDTIwMDgwMjE3MTEy +MVoXDTQ3MTIyMDE3MTEyMVowFjEUMBIGA1UEAwwLdWdseSBjbGllbnQwggGiMA0G +CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDGChdOMY164FScJqfiJ5LEtjYOKEg4 +nmMAMGuHIT8wZZEfzaaHhBbypzKq2cPP1qtyHgvtUMM6KOFEj4y9AonqzzdlVxbM +i6+AvYLWlPoB5r/G1GdslUvXbc7F02B/6sB/+iFXmcdjOjAQcLWxVgUL+1CoeoY1 +awNYmzQueK/T82a/6AYTdrx7XRX4wfxjYb1o3bnnRD/jGSakIylXeUGFsiSNkBs/ +dJMkUONxizAdAE2tW6NhPuE2O0UipzUhdgFnH6WPfJ0J1S7jZ3eQTUrLkFpWSp/Z +hx/l/Ql9vO0wagHaT2wOiZdKVT8S6V6OPzJ7/H1evCoM6EuSPBC5DDP1nPetCK1v +uC9kb7Dg6yFPt1CKrVFt0Y6W5Y5/GzisUtvYV/OGtX4DOwL9It68D04Qrvun1t/a +Dh/c5gKqfqIGHUvUucFmOi6DrRpadfraLZMRGN2ysPjoVwhMgwwSmSWhziQIUfxK +oyz1CUsyr5Gh5gdifbe1AOYwu6YdtlmhqCsCAwEAAaM2MDQwHQYDVR0OBBYEFINV +dy1eIfFJDAkk51QJEo3IfgSuMBMGA1UdJQQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3 +DQEBCwUAA4IBgQAhAEACc1j6EYoSfVJD8N/FlYfHRizdfVJyrmMnC8ID1vtfrU2z +S2q+49ja2NyM4Sq+Cf+i+sFfzFG92LayZt9Mc1BnHZMdNzQL7Ynr2nDLxHsHzuYa +N21/ucTpHEFGLmvQ/eWBMxQQ9TbiNXn+tnnqg46dRzN3vHJp+g5+ijtMcuh007z2 +niiO8F07wlb960XviejWejMC8hBLWlA7i3EjAkDO8RFQnG2Py5cQX9GgmWH1sDy3 +rIsWlU+e46ysSWK/bnudnAlzZMB9KJATVZu5+xmCumH2hLJv5vz+jnKcgU9MBZMO +cKgNdFUbtRlU/gfTaohmLIuSquunCCrXLsLD8ygbKKXfSPGVo2XkvX3oxqUo6dmA +LvU4N4sCQGiSzW+a13HBtk3TBZFsJSWUGSW/H7TVFiAonumJKRqRxMOkkB9JxX+V +9LZBYuBLpOeK4wZ8BUSNlHKnGpDzl0DzdYrGlzWz0jXlLGZ8KMfXAn9h0mOZ+IyK +eUlgMBYyAspCQzM= +-----END CERTIFICATE----- +""" + +client_key_pem = normalize_privatekey_pem( + b"""-----BEGIN RSA PRIVATE KEY----- +MIIG5AIBAAKCAYEAxgoXTjGNeuBUnCan4ieSxLY2DihIOJ5jADBrhyE/MGWRH82m +h4QW8qcyqtnDz9arch4L7VDDOijhRI+MvQKJ6s83ZVcWzIuvgL2C1pT6Aea/xtRn +bJVL123OxdNgf+rAf/ohV5nHYzowEHC1sVYFC/tQqHqGNWsDWJs0Lniv0/Nmv+gG +E3a8e10V+MH8Y2G9aN2550Q/4xkmpCMpV3lBhbIkjZAbP3STJFDjcYswHQBNrVuj +YT7hNjtFIqc1IXYBZx+lj3ydCdUu42d3kE1Ky5BaVkqf2Ycf5f0JfbztMGoB2k9s +DomXSlU/Eulejj8ye/x9XrwqDOhLkjwQuQwz9Zz3rQitb7gvZG+w4OshT7dQiq1R +bdGOluWOfxs4rFLb2FfzhrV+AzsC/SLevA9OEK77p9bf2g4f3OYCqn6iBh1L1LnB +Zjoug60aWnX62i2TERjdsrD46FcITIMMEpkloc4kCFH8SqMs9QlLMq+RoeYHYn23 +tQDmMLumHbZZoagrAgMBAAECggGAAXA5UxwRBv9yHeA5/+6BpmQcaGXqgF7GIU44 +ubaIGvXh4/U+bGWNNR35xDvorC3G+QE23PZlNJrvZ+wS/ZxzG/19TYMga0Podmrp +9F0Io9LlObB5P9SlxF7LzawHW2Z9F3DdpSE8zX+ysavf5fXV+4xLva2GJAUu9QnL +izrdLBDsgiBRSvrly4+VhUUDbEVddtGFdCSOwjuAiFipCDWdQDdXBKAzUnaqSu07 +eaulIdDKv6OWwDIQuLAdhG7qd9+/h5MB/rAG8v4bgbHz1H/RZw5VIOuOhfCodzJx +3Smfh5td21jwJ2RfZYEPNOMtFa9eRFtH2/uRa5jbJiZb8YWIzWy0xCNQpKheSoBO +wiuMDBS2HCYm2SgEYDdJiE2OkRAk0UwTiUmlmZd0a3NfJ/rfQE+JiDQ28Arj3EZl +SY/V3KdviM4MbaoX7f9j9sjAe5Rk1M+yI8OsnM/hf77m0CSiJJpLpvgqhMjjT+NI +aBm1FyTq6qu506d0YUZy+Wr2DRsBAoHBAPfshuOiDXo9UmJxM1mSJQ0rQlxWSWmX +bIAsPrpKslTFYHk7xbcCbJCqMbHmCsyvYy3oW3SpJs6Vi2wQWuUQmsm0YC7qdkXF +Fyo2f7vF7roQcXLxVmQRo0OxZ9JpLAZ9DKMEcNfYyUiQiqJmZuIyWKngqBl6OoL2 +8EJSFjTY1tR/nDxGLpZSsqoZJWQGd9B2bK4y6NktDF1GkexCpKaSyXZT612JGPG2 +0gSIwRq1OgZH3SPHevhVMjJtxGue2XARywKBwQDMfZHtdJI9RuurM9UuULZ72SmW +oLzki3LwFQ/QoS9wrHK+OqQDWH2ddON1PoB4LOCpwB4CC83pMyfxurgLHut6saHL +hQ5N+h0jUC2pSJOXZSfF2Hx8YHCT7Dga5kmgEy89c1TF48IL2LdUZQQIGZt8+FxM +4nxT9NFlu/UWY2oftT+ZwFsIock/DYYUKxDXw9YkOmt1lO5u1SKte0NdQ4RhBeqK +nRADMSS9oKZkSUxkwaDJH2GkUVTyBsF/kmh+dyECgcEA6jy3yRQPxcFwOAAZ8vOo +PAP2I8WGgNQHOCYVce8nA/6jwocdq2YH6rpST3E4HOFMRFB3MAas2pvh6UyehDOm ++xGHmmv9KLgoxcJN9rvwbC0i8uVfqRYc+dUAcYTaiprVOKP2dYilzAB8ayly5R2K +NZ5DVCbuZ1Ql9ZMW1gFVH9odY7kvROmHUjyF3jZaN0PcNM12v9HXD72gGudwJs0i +uMBa7LmeLql7TbtjLvewhcSaA7bx0PS1g33ACapAZ6j3AoHAN2PsGz3wPtjvDTjF +Df6e730rXrm7cMy1HYMW/ZQrnYGYsx5/PsjBfd0jn6aGdgbx9AkuF6/K3tgUgc3p +/Fkrv9hN0yr/bO/K5L3bIHegQuoLk/PIBIi69daOe/rVBp8rtKGA3PmMnljdj+as +6OTG0VsU5V6T/snZzozTHnVfUaduyt7nybbJJGMtZlkj/s31O2r3oKnuy+a/te4l +mSWovf80QMe6hqLRKOxTJecU4lXwj4oIkNHXCJf74epuk5MBAoHBALyvg90KzMFX +ZEjdPIXULR6/3rub8yD7LVYbNhhYWGo8GybzsBUC0kczRpRXFnmbq1GDIXQf5A+2 +3ZaGsWzAxLjvL3KwH1LUaXVWwFMOM2n6zTk18XEXrNvp+E5QtPwpO5c4VlPr0cAC +tTPAmbu6kVPlQ6mKiqlPAsfh0BD2mRVo2cTjZgDotKshb5uCHD8/PnCfOjCXFxOf +DWjBuR73/r5Bj+ktRoD4V2SFdO6loJwH6B8rsBjD0NbAGs9otKvy+Q== +-----END RSA PRIVATE KEY----- +""" +) + +encryptedPrivateKeyPEM = b"""-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,9573604A18579E9E + +SHOho56WxDkT0ht10UTeKc0F5u8cqIa01kzFAmETw0MAs8ezYtK15NPdCXUm3X/2 +a17G7LSF5bkxOgZ7vpXyMzun/owrj7CzvLxyncyEFZWvtvzaAhPhvTJtTIB3kf8B +8+qRcpTGK7NgXEgYBW5bj1y4qZkD4zCL9o9NQzsKI3Ie8i0239jsDOWR38AxjXBH +mGwAQ4Z6ZN5dnmM4fhMIWsmFf19sNyAML4gHenQCHhmXbjXeVq47aC2ProInJbrm ++00TcisbAQ40V9aehVbcDKtS4ZbMVDwncAjpXpcncC54G76N6j7F7wL7L/FuXa3A +fvSVy9n2VfF/pJ3kYSflLHH2G/DFxjF7dl0GxhKPxJjp3IJi9VtuvmN9R2jZWLQF +tfC8dXgy/P9CfFQhlinqBTEwgH0oZ/d4k4NVFDSdEMaSdmBAjlHpc+Vfdty3HVnV +rKXj//wslsFNm9kIwJGIgKUa/n2jsOiydrsk1mgH7SmNCb3YHgZhbbnq0qLat/HC +gHDt3FHpNQ31QzzL3yrenFB2L9osIsnRsDTPFNi4RX4SpDgNroxOQmyzCCV6H+d4 +o1mcnNiZSdxLZxVKccq0AfRpHqpPAFnJcQHP6xyT9MZp6fBa0XkxDnt9kNU8H3Qw +7SJWZ69VXjBUzMlQViLuaWMgTnL+ZVyFZf9hTF7U/ef4HMLMAVNdiaGG+G+AjCV/ +MbzjS007Oe4qqBnCWaFPSnJX6uLApeTbqAxAeyCql56ULW5x6vDMNC3dwjvS/CEh +11n8RkgFIQA0AhuKSIg3CbuartRsJnWOLwgLTzsrKYL4yRog1RJrtw== +-----END RSA PRIVATE KEY----- +""" + +encryptedPrivateKeyPEMPassphrase = b"foobar" + +cleartextPrivateKeyPEM = """-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMcRMugJ4kvkOEuT +AvMFr9+3A6+HAB6nKYcXXZz93ube8rJpBZQEfWn73H10dQiQR/a+rhxYEeLy8dPc +UkFcGR9miVkukJ59zex7iySJY76bdBD8gyx1LTKrkCstP2XHKEYqgbj+tm7VzJnY +sQLqoaa5NeyWJnUC3MJympkAS7p3AgMBAAECgYAoBAcNqd75jnjaiETRgVUnTWzK +PgMCJmwsob/JrSa/lhWHU6Exbe2f/mcGOQDFpesxaIcrX3DJBDkkc2d9h/vsfo5v +JLk/rbHoItWxwuY5n5raAPeQPToKpTDxDrL6Ejhgcxd19wNht7/XSrYZ+dq3iU6G +mOEvU2hrnfIW3kwVYQJBAP62G6R0gucNfaKGtHzfR3TN9G/DnCItchF+TxGTtpdh +Cz32MG+7pirT/0xunekmUIp15QHdRy496sVxWTCooLkCQQDIEwXTAwhLNRGFEs5S +jSkxNfTVeNiOzlG8jPBJJDAdlLt1gUqjZWnk9yU+itMSGi/6eeuH2n04FFk+SV/T +7ryvAkB0y0ZDk5VOozX/p2rtc2iNm77A3N4kIdiTQuq4sZXhNgN0pwWwxke8jbcb +8gEAnqwBwWt//locTxHu9TmjgT8pAkEAlbF16B0atXptM02QxT8MlN8z4gxaqu4/ +RX2FwpOq1FcVsqMbvwj/o+ouGY8wwRiK0TMrQCf/DFhdNTcc1aqHzQJBAKWtq4LI +uVZjCAuyrqEnt7R1bOiLrar+/ezJPY2z+f2rb1TGr31ztPeFvO3edLw+QdhzwJGp +QKImYzqMe+zkIOQ= +-----END PRIVATE KEY----- +""" + +cleartextPublicKeyPEM = b"""-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxszlc+b71LvlLS0ypt/l +gT/JzSVJtnEqw9WUNGeiChywX2mmQLHEt7KP0JikqUFZOtPclNY823Q4pErMTSWC +90qlUxI47vNJbXGRfmO2q6Zfw6SE+E9iUb74xezbOJLjBuUIkQzEKEFV+8taiRV+ +ceg1v01yCT2+OjhQW3cxG42zxyRFmqesbQAUWgS3uhPrUQqYQUEiTmVhh4FBUKZ5 +XIneGUpX1S7mXRxTLH6YzRoGFqRoc9A0BBNcoXHTWnxV215k4TeHMFYE5RG0KYAS +8Xk5iKICEXwnZreIt3jyygqoOKsKZMK/Zl2VhMGhJR6HXRpQCyASzEG7bgtROLhL +ywIDAQAB +-----END PUBLIC KEY----- +""" + +# A broken RSA private key which can be used to test the error path through +# PKey.check. +inconsistentPrivateKeyPEM = b"""-----BEGIN RSA PRIVATE KEY----- +MIIBPAIBAAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh +5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEaAQJBAIqm/bz4NA1H++Vx5Ewx +OcKp3w19QSaZAwlGRtsUxrP7436QjnREM3Bm8ygU11BjkPVmtrKm6AayQfCHqJoT +zIECIQDW0BoMoL0HOYM/mrTLhaykYAVqgIeJsPjvkEhTFXWBuQIhAM3deFAvWNu4 +nklUQ37XsCT2c9tmNt1LAT+slG2JOTTRAiAuXDtC/m3NYVwyHfFm+zKHRzHkClk2 +HjubeEgjpj32AQIhAJqMGTaZVOwevTXvvHwNeH+vRWsAYU/gbx+OQB+7VOcBAiEA +oolb6NMg/R3enNPvS1O4UU1H8wpaF77L4yiSWlE0p4w= +-----END RSA PRIVATE KEY----- +""" + +# certificate with NULL bytes in subjectAltName and common name + +nulbyteSubjectAltNamePEM = b"""-----BEGIN CERTIFICATE----- +MIIE2DCCA8CgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBxTELMAkGA1UEBhMCVVMx +DzANBgNVBAgMBk9yZWdvbjESMBAGA1UEBwwJQmVhdmVydG9uMSMwIQYDVQQKDBpQ +eXRob24gU29mdHdhcmUgRm91bmRhdGlvbjEgMB4GA1UECwwXUHl0aG9uIENvcmUg +RGV2ZWxvcG1lbnQxJDAiBgNVBAMMG251bGwucHl0aG9uLm9yZwBleGFtcGxlLm9y +ZzEkMCIGCSqGSIb3DQEJARYVcHl0aG9uLWRldkBweXRob24ub3JnMB4XDTEzMDgw +NzEzMTE1MloXDTEzMDgwNzEzMTI1MlowgcUxCzAJBgNVBAYTAlVTMQ8wDQYDVQQI +DAZPcmVnb24xEjAQBgNVBAcMCUJlYXZlcnRvbjEjMCEGA1UECgwaUHl0aG9uIFNv +ZnR3YXJlIEZvdW5kYXRpb24xIDAeBgNVBAsMF1B5dGhvbiBDb3JlIERldmVsb3Bt +ZW50MSQwIgYDVQQDDBtudWxsLnB5dGhvbi5vcmcAZXhhbXBsZS5vcmcxJDAiBgkq +hkiG9w0BCQEWFXB5dGhvbi1kZXZAcHl0aG9uLm9yZzCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBALXq7cn7Rn1vO3aA3TrzA5QLp6bb7B3f/yN0CJ2XFj+j +pHs+Gw6WWSUDpybiiKnPec33BFawq3kyblnBMjBU61ioy5HwQqVkJ8vUVjGIUq3P +vX/wBmQfzCe4o4uM89gpHyUL9UYGG8oCRa17dgqcv7u5rg0Wq2B1rgY+nHwx3JIv +KRrgSwyRkGzpN8WQ1yrXlxWjgI9de0mPVDDUlywcWze1q2kwaEPTM3hLAmD1PESA +oY/n8A/RXoeeRs9i/Pm/DGUS8ZPINXk/yOzsR/XvvkTVroIeLZqfmFpnZeF0cHzL +08LODkVJJ9zjLdT7SA4vnne4FEbAxDbKAq5qkYzaL4UCAwEAAaOB0DCBzTAMBgNV +HRMBAf8EAjAAMB0GA1UdDgQWBBSIWlXAUv9hzVKjNQ/qWpwkOCL3XDALBgNVHQ8E +BAMCBeAwgZAGA1UdEQSBiDCBhYIeYWx0bnVsbC5weXRob24ub3JnAGV4YW1wbGUu +Y29tgSBudWxsQHB5dGhvbi5vcmcAdXNlckBleGFtcGxlLm9yZ4YpaHR0cDovL251 +bGwucHl0aG9uLm9yZwBodHRwOi8vZXhhbXBsZS5vcmeHBMAAAgGHECABDbgAAAAA +AAAAAAAAAAEwDQYJKoZIhvcNAQEFBQADggEBAKxPRe99SaghcI6IWT7UNkJw9aO9 +i9eo0Fj2MUqxpKbdb9noRDy2CnHWf7EIYZ1gznXPdwzSN4YCjV5d+Q9xtBaowT0j +HPERs1ZuytCNNJTmhyqZ8q6uzMLoht4IqH/FBfpvgaeC5tBTnTT0rD5A/olXeimk +kX4LxlEx5RAvpGB2zZVRGr6LobD9rVK91xuHYNIxxxfEGE8tCCWjp0+3ksri9SXx +VHWBnbM9YaL32u3hxm8sYB/Yb8WSBavJCWJJqRStVRHM1koZlJmXNx2BX4vPo6iW +RFEIPQsFZRLrtnCAiEhyT8bC2s/Njlu6ly9gtJZWSV46Q3ZjBL4q9sHKqZQ= +-----END CERTIFICATE-----""" + +large_key_pem = b"""-----BEGIN RSA PRIVATE KEY----- +MIIJYgIBAAKCAg4AtRua8eIeevRfsj+fkcHr1vmse7Kgb+oX1ssJAvCb1R7JQMnH +hNDjDP6b3vEkZuPUzlDHymP+cNkXvvi4wJ4miVbO3+SeU4Sh+jmsHeHzGIXat9xW +9PFtuPM5FQq8zvkY8aDeRYmYwN9JKu4/neMBCBqostYlTEWg+bSytO/qWnyHTHKh +g0GfaDdqUQPsGQw+J0MgaYIjQOCVASHAPlzbDQLCtuOb587rwTLkZA2GwoHB/LyJ +BwT0HHgBaiObE12Vs6wi2en0Uu11CiwEuK1KIBcZ2XbE6eApaZa6VH9ysEmUxPt7 +TqyZ4E2oMIYaLPNRxuvozdwTlj1svI1k1FrkaXGc5MTjbgigPMKjIb0T7b/4GNzt +DhP1LvAeUMnrEi3hJJrcJPXHPqS8/RiytR9xQQW6Sdh4LaA3f9MQm3WSevWage3G +P8YcCLssOVKsArDjuA52NF5LmYuAeUzXprm4ITDi2oO+0iFBpFW6VPEK4A9vO0Yk +M/6Wt6tG8zyWhaSH1zFUTwfQ9Yvjyt5w1lrUaAJuoTpwbMVZaDJaEhjOaXU0dyPQ +jOsePDOQcU6dkeTWsQ3LsHPEEug/X6819TLG5mb3V7bvV9nPFBfTJSCEG794kr90 +XgZfIN71FrdByxLerlbuJI21pPs/nZi9SXi9jAWeiS45/azUxMsyYgJArui+gjq7 +sV1pWiBm6/orAgMBAAECggINQp5L6Yu+oIXBqcSjgq8tfF9M5hd30pLuf/EheHZf +LA7uAqn2fVGFI2OInIJhXIOT5OxsAXO0xXfltzawZxIFpOFMqajj4F7aYjvSpw9V +J4EdSiJ/zgv8y1qUdbwEZbHVThRZjoSlrtSzilonBoHZAE0mHtqMz7iRFSk1zz6t +GunRrvo/lROPentf3TsvHquVNUYI5yaapyO1S7xJhecMIIYSb8nbsHI54FBDGNas +6mFmpPwI/47/6HTwOEWupnn3NicsjrHzUInOUpaMig4cRR+aP5bjqg/ty8xI8AoN +evEmCytiWTc+Rvbp1ieN+1jpjN18PjUk80/W7qioHUDt4ieLic8uxWH2VD9SCEnX +Mpi9tA/FqoZ+2A/3m1OfrY6jiZVE2g+asi9lCK7QVWL39eK82H4rPvtp0/dyo1/i +ZZz68TXg+m8IgEZcp88hngbkuoTTzpGE73QuPKhGA1uMIimDdqPPB5WP76q+03Oi +IRR5DfZnqPERed49by0enJ7tKa/gFPZizOV8ALKr0Dp+vfAkxGDLPLBLd2A3//tw +xg0Q/wltihHSBujv4nYlDXdc5oYyMYZ+Lhc/VuOghHfBq3tgEQ1ECM/ofqXEIdy7 +nVcpZn3Eeq8Jl5CrqxE1ee3NxlzsJHn99yGQpr7mOhW/psJF3XNz80Meg3L4m1T8 +sMBK0GbaassuJhdzb5whAoIBBw48sx1b1WR4XxQc5O/HjHva+l16i2pjUnOUTcDF +RWmSbIhBm2QQ2rVhO8+fak0tkl6ZnMWW4i0U/X5LOEBbC7+IS8bO3j3Revi+Vw5x +j96LMlIe9XEub5i/saEWgiz7maCvfzLFU08e1OpT4qPDpP293V400ubA6R7WQTCv +pBkskGwHeu0l/TuKkVqBFFUTu7KEbps8Gjg7MkJaFriAOv1zis/umK8pVS3ZAM6e +8w5jfpRccn8Xzta2fRwTB5kCmfxdDsY0oYGxPLRAbW72bORoLGuyyPp/ojeGwoik +JX9RttErc6FjyZtks370Pa8UL5QskyhMbDhrZW2jFD+RXYM1BrvmZRjbAoIBBwy4 +iFJpuDfytJfz1MWtaL5DqEL/kmiZYAXl6hifNhGu5GAipVIIGsDqEYW4i+VC15aa +7kOCwz/I5zsB3vSDW96IRs4wXtqEZSibc2W/bqfVi+xcvPPl1ZhQ2EAwa4D/x035 +kyf20ffWOU+1yf2cnijzqs3IzlveUm+meLw5s3Rc+iG7DPWWeCoe1hVwANI1euNc +pqKwKY905yFyjOje2OgiEU2kS4YME4zGeBys8yo7E42hNnN2EPK6xkkUqzdudLLQ +8OUlKRTc8AbIf3XG1rpA4VUpTv3hhxGGwCRy6If8zgZQsNYchgNztRGk72Gcb8Dm +vFSEN3ZtwxU64G3YZzntdcr2WPzxAoIBBw30g6Fgdb/gmVnOpL0//T0ePNDKIMPs +jVJLaRduhoZgB1Bb9qPUPX0SzRzLZtg1tkZSDjBDoHmOHJfhxUaXt+FLCPPbrE4t ++nq9n/nBaMM779w9ClqhqLOyGrwKoxjSmhi+TVEHyIxCbXMvPHVHfX9WzxjbcGrN +ZvRaEVZWo+QlIX8yqdSwqxLk1WtAIRzvlcj7NKum8xBxPed6BNFep/PtgIAmoLT5 +L8wb7EWb2iUdc2KbZ4OaY51lDScqpATgXu3WjXfM+Q52G0mX6Wyd0cjlL711Zrjb +yLbiueZT94lgIHHRRKtKc8CEqcjkQV5OzABS3P/gQSfgZXBdLKjOpTnKDUq7IBeH +AoIBBweAOEIAPLQg1QRUrr3xRrYKRwlakgZDii9wJt1l5AgBTICzbTA1vzDJ1JM5 +AqSpCV6w9JWyYVcXK+HLdKBRZLaPPNEQDJ5lOxD6uMziWGl2rg8tj+1xNMWfxiPz +aTCjoe4EoBUMoTq2gwzRcM2usEQNikXVhnj9Wzaivsaeb4bJ3GRPW5DkrO6JSEtT +w+gvyMqQM2Hy5k7E7BT46sXVwaj/jZxuqGnebRixXtnp0WixdRIqYWUr1UqLf6hQ +G7WP2BgoxCMaCmNW8+HMD/xuxucEotoIhZ+GgJKBFoNnjl3BX+qxYdSe9RbL/5Tr +4It6Jxtj8uETJXEbv9Cg6v1agWPS9YY8RLTBAoIBBwrU2AsAUts6h1LgGLKK3UWZ +oLH5E+4o+7HqSGRcRodVeN9NBXIYdHHOLeEG6YNGJiJ3bFP5ZQEu9iDsyoFVKJ9O +Mw/y6dKZuxOCZ+X8FopSROg3yWfdOpAm6cnQZp3WqLNX4n/Q6WvKojfyEiPphjwT +0ymrUJELXLWJmjUyPoAk6HgC0Gs28ZnEXbyhx7CSbZNFyCU/PNUDZwto3GisIPD3 +le7YjqHugezmjMGlA0sDw5aCXjfbl74vowRFYMO6e3ItApfSRgNV86CDoX74WI/5 +AYU/QVM4wGt8XGT2KwDFJaxYGKsGDMWmXY04dS+WPuetCbouWUusyFwRb9SzFave +vYeU7Ab/ +-----END RSA PRIVATE KEY-----""" + +ec_private_key_pem = b"""-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYirTZSx+5O8Y6tlG +cka6W6btJiocdrdolfcukSoTEk+hRANCAAQkvPNu7Pa1GcsWU4v7ptNfqCJVq8Cx +zo0MUVPQgwJ3aJtNM1QMOQUayCrRwfklg+D/rFSUwEUqtZh7fJDiFqz3 +-----END PRIVATE KEY----- +""" +ec_public_key_pem = b"""-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJLzzbuz2tRnLFlOL+6bTX6giVavA +sc6NDFFT0IMCd2ibTTNUDDkFGsgq0cH5JYPg/6xUlMBFKrWYe3yQ4has9w== +-----END PUBLIC KEY----- +""" + +ec_root_key_pem = b"""-----BEGIN EC PRIVATE KEY----- +MIGlAgEBBDEAz/HOBFPYLB0jLWeTpJn4Yc4m/C4mdWymVHBjOmnwiPHKT326iYN/ +ZhmSs+RM94RsoAcGBSuBBAAioWQDYgAEwE5vDdla/nLpWAPAQ0yFGqwLuw4BcN2r +U+sKab5EAEHzLeceRa8ffncYdCXNoVsBcdob1y66CFZMEWLetPTmGapyWkBAs6/L +8kUlkU9OsE+7IVo4QQJkgV5gM+Dim1XE +-----END EC PRIVATE KEY----- +""" + +ec_root_cert_pem = b"""-----BEGIN CERTIFICATE----- +MIICLTCCAbKgAwIBAgIMWW/hwTl6ufz6/WkCMAoGCCqGSM49BAMDMFgxGDAWBgNV +BAMTD1Rlc3RpbmcgUm9vdCBDQTEQMA4GA1UEChMHVGVzdGluZzEQMA4GA1UEBxMH +Q2hpY2FnbzELMAkGA1UECBMCSUwxCzAJBgNVBAYTAlVTMCAXDTE3MDcxOTIyNDgz +M1oYDzk5OTkxMjMxMjM1OTU5WjBYMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0Ex +EDAOBgNVBAoTB1Rlc3RpbmcxEDAOBgNVBAcTB0NoaWNhZ28xCzAJBgNVBAgTAklM +MQswCQYDVQQGEwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABMBObw3ZWv5y6VgD +wENMhRqsC7sOAXDdq1PrCmm+RABB8y3nHkWvH353GHQlzaFbAXHaG9cuughWTBFi +3rT05hmqclpAQLOvy/JFJZFPTrBPuyFaOEECZIFeYDPg4ptVxKNDMEEwDwYDVR0T +AQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwQAMB0GA1UdDgQWBBSoTrF0H2m8RDzB +MnY2KReEPfz7ZjAKBggqhkjOPQQDAwNpADBmAjEA3+G1oVCxGjYX4iUN93QYcNHe +e3fJQJwX9+KsHRut6qNZDUbvRbtO1YIAwB4UJZjwAjEAtXCPURS5A4McZHnSwgTi +Td8GMrwKz0557OxxtKN6uVVy4ACFMqEw0zN/KJI1vxc9 +-----END CERTIFICATE-----""" + +rsa_p_not_prime_pem = """ +-----BEGIN RSA PRIVATE KEY----- +MBsCAQACAS0CAQcCAQACAQ8CAQMCAQACAQACAQA= +-----END RSA PRIVATE KEY----- +""" + +dsa_private_key_pem = b"""-----BEGIN PRIVATE KEY----- +MIICZAIBADCCAjkGByqGSM44BAEwggIsAoIBAQD7UzdlshSCIIuntch43VmfCX1+ +WQDTvGw83sRZcN+B7nwFn4dm2PU8cby17oCjX7buBvalVqofnUokrSIDA6Rozm/f +2wpGR9oVpd0xh9cI50pw1G3RZ4lcNWTP8C8O20eIzJoCH1KElcWLCHLAa3XoGOMv +p4XnbVgMdc9/ydt4qttzIVPV4cZoVObzixoKCgwHyVPDxe0JaCe2cIwxyQY0IwAI +PfaUWEAo+bf7pOosdnatJYm9MkKe8bEgKGQcUl9S8FXLhRejMo+oobcRjuBHTAmY +fuV1iGlLrkFNrc2O6M1CRZhOoddoy53IeHcSjfzKET1biE3tCOUdHjUnABqfAiEA +1llvauVKMLvFCDatVKRY+zNGJaa5dwff4qDtodz6sa8CggEAd+btod0di21mqFaf +vc1ddmLK74PddMseT8DmoN/YduJaGLAOOVJ61rdG+KPXIar+8X5yqXfzP0MiYGkE +A+xpNIImC3rzHElYNa8imA7ud8f+oC5jQijp0GhzVIS4UW83rZwakX7LITNE9Oj9 +FkETH1ZskHpp5BNlNoaSIW2+T7n/a+lq+tN60gP3f6FPBv5obB0pjqh+OAzEil/4 +Ys0dtCB0022cCUCqThMhWewlE2W2JioDLV5QkD91NMQNQwljDONNcs94AaWeVONK +RaBQXlFsJPHzS8uKpsFeusFTrHIeEJW/8GQp/tfXP1ajEdg5EGxOhXFkem4ZMIus +YFTbWwQiAiBFtgi8aNV0Jz2o8T+cxjVqVEgGdYNQqmpzqqBsM5AEOw== +-----END PRIVATE KEY----- +""" +dsa_public_key_pem = b"""-----BEGIN PUBLIC KEY----- +MIIDRjCCAjkGByqGSM44BAEwggIsAoIBAQD7UzdlshSCIIuntch43VmfCX1+WQDT +vGw83sRZcN+B7nwFn4dm2PU8cby17oCjX7buBvalVqofnUokrSIDA6Rozm/f2wpG +R9oVpd0xh9cI50pw1G3RZ4lcNWTP8C8O20eIzJoCH1KElcWLCHLAa3XoGOMvp4Xn +bVgMdc9/ydt4qttzIVPV4cZoVObzixoKCgwHyVPDxe0JaCe2cIwxyQY0IwAIPfaU +WEAo+bf7pOosdnatJYm9MkKe8bEgKGQcUl9S8FXLhRejMo+oobcRjuBHTAmYfuV1 +iGlLrkFNrc2O6M1CRZhOoddoy53IeHcSjfzKET1biE3tCOUdHjUnABqfAiEA1llv +auVKMLvFCDatVKRY+zNGJaa5dwff4qDtodz6sa8CggEAd+btod0di21mqFafvc1d +dmLK74PddMseT8DmoN/YduJaGLAOOVJ61rdG+KPXIar+8X5yqXfzP0MiYGkEA+xp +NIImC3rzHElYNa8imA7ud8f+oC5jQijp0GhzVIS4UW83rZwakX7LITNE9Oj9FkET +H1ZskHpp5BNlNoaSIW2+T7n/a+lq+tN60gP3f6FPBv5obB0pjqh+OAzEil/4Ys0d +tCB0022cCUCqThMhWewlE2W2JioDLV5QkD91NMQNQwljDONNcs94AaWeVONKRaBQ +XlFsJPHzS8uKpsFeusFTrHIeEJW/8GQp/tfXP1ajEdg5EGxOhXFkem4ZMIusYFTb +WwOCAQUAAoIBAEe6z5ud1k4EDD9mLP7UYALWrgc1NXUlDynoYkjr+T/NVf1eaMdq +0vFbGcEmz05UPUNXOhDH0szUDxQam3IE9C27ZO4SOquc0/rIhPY6i75SJW13P+cg +gdXhDMTW5JOlyV6CPUoCWKOtn1ds3pTDuuWlZ89UzOWQUbC1si6vvz43zDyhfu6U +owgIusPxowErm2sH66+MPa8fYxVX7ZJL0mEfubejrloAbo5unYI/bUYIhx4mtpP/ +h/isFRifEAwG3yX6F9X/ZOYL53Z93EFPLJGRGMmQbkmXRA6lyvHdsC+OC/OCvPjW +WfTXW9NHtUqpEks+OXBkyV971Hk5NvdLLr8= +-----END PUBLIC KEY----- +""" + +ed25519_private_key_pem = b"""-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIKlxBbhVsSURoLTmsu9uTqYH6oF7zpxmp1ZQCAPhDmI2 +-----END PRIVATE KEY----- +""" +ed25519_public_key_pem = b"""-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAq+FrpdwI1oTPytx8kGzuLVc+78zJE7hjYG4E9hwXoKI= +-----END PUBLIC KEY----- +""" + +ed448_private_key_pem = b"""-----BEGIN PRIVATE KEY----- +MEcCAQAwBQYDK2VxBDsEOcqZ7a3k6JwrJbYO8CNTPT/d7dlWCo5vCf0EYDj79ZvA\nhD8u9EPHlYJw5Y8ZQdH4WmVEfpKA23xkdQ== +-----END PRIVATE KEY----- +""" +ed448_public_key_pem = b"""-----BEGIN PUBLIC KEY----- +MEMwBQYDK2VxAzoAKFfWGCuqIaxgR9GmEXLRciYDyEjTnF56kr0sOVfwHEj+bHSU\neMJTZJR8qFSg8hNsHY1iZh9PIXcA +-----END PUBLIC KEY----- +""" + +rsa_private_key_pem = b"""-----BEGIN PRIVATE KEY----- +MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDZ5FaSaXKn/RTF +xyNr+GRvYnMvLz5XxSDD4JzVRKXxKGFzKKXMJAeXJkvPlho7Ta/HgMNXhMPAe8TT +wcIRnHJqAfmSOnka1ks3Kl6EGQBTevKzyJy8MaUhzZsL4FUUgWUETFQQT8Dwcghf +JobV0k+bWT4mrKHzIquw5y+NTsaZl4jSB1labhImsU16Vj66fHp7w9+c501tOxQO +M4CQNWioGm8tgPT/43QUs9e+L2HFBI+cDQbEC68l+7VM8YY8NZ/fGypoML2QMVnU +Y6zneoOLJTMUulOubrL+J6DkuuhxBsIOcyxMnqwgKm4pUGlPxfPSS7+Mo3JC969k +wgUHerXZAgMBAAECgf9qAzz/VMCQwnV1UxkhxH/8zgYgYL+fERFuPC/ZWv7wOicv +xAjm9KC8zVb44fLE586CCc7IN+zNK9y0gB9eAGr/04RhEvWgbmoqF6wdtdNyynuE +Ut4oQKn7AUc1uPAeCfM4slw0Pie98YSS/9ZhwH/eh3C10iwWA1aiLWeDrnryPuJN +mNB0d/ZsaL+arhR/nU2sJixx5LDI6AG0GJrw3DBHEKb4vZPIUM3wZNs7qnuG5W17 +JbZDQYnkApByZu2UMWI2YUkpJC246mFPWSWMa6sAl7sTWTkUIR21lJiqyTGG3ljY +C2QjHoHrrzs+pwtlLBa1a4FgbaJmnL+VzWD/FQECgYEA8r3Y2oGcY5cQPb00TE0t +ekXAXiHz9sX76nzE6BMZ8cwP/cVoWtIABpdaimKUoFML8CdjOi9Ti9OoNVGWm4Pk +fT/GOUdysXWIw2Z/VOLM47nDwJb3fWwxsxph+x3gWJG/Vct/1NxmCCEendM63dy7 +/uR8RgX+0nxvn6Y6auQfpnkCgYEA5csHboa14Favx8aHTlITWOm46ugzdbARdfWz +13Ewb7m4mm/3gKtA/m+yGdQFwmtBVkmwtdCeDj0aKH3Sfvg9WCQK1x/dUkPMr//r +oGUGeJU9r3ZKVJTeSJ0lKX4h3u3+1TdpnAgtuWGI4AK9fEdulfHKArxyIdbsdwRr +ljaBMmECgYATpEcCz1APQu7+f+vWbLxMU46QT2EFS9npjHUGbl1AEooMt8eM6cc0 +wVSDNBzgqDekFBvUXnX9L4BB6DsulEqN0/Y/NkfSkjch0I5nGP8JQkPTtqOKE5Il +8vGQt0crA4ge8huC5t6es8ddb/UodK8FnglsRRnsgEMsAPBjK9hfyQKBgDHD23Mr +R14zR9Q7AXiLu9bonvx4lxRosg9ay7zfrX60uO7xSqeZ7vRrWiXPzgOB2N+IC/YE +HQa2YuDcBucqeZaKD7LxGqxDNKP1B6Fv34vjvj0uoABbURxms/Kdd1ZhMmwYmQ2K +k+Ru5AancUPl8GQWvgoDp6/+bK2Fzor0eNxhAoGBANcJ6mGvgw3px/H2MPBjRBsf +tUbZ39UH3c4siLa2Rry/Pm0Fgly8CUmu1IcFQDITKbyhaGPuHGtXglBOZqXid0VL +01ReWISyKwWyuRjUuscdq2m684hXHYZCq2eJroqon1nMq4C0aqr696ra0cgCfbK3 +5yscAByxKd+64JZziDkZ +-----END PRIVATE KEY----- +""" +rsa_public_key_pem = b"""-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2eRWkmlyp/0Uxccja/hk +b2JzLy8+V8Ugw+Cc1USl8ShhcyilzCQHlyZLz5YaO02vx4DDV4TDwHvE08HCEZxy +agH5kjp5GtZLNypehBkAU3rys8icvDGlIc2bC+BVFIFlBExUEE/A8HIIXyaG1dJP +m1k+Jqyh8yKrsOcvjU7GmZeI0gdZWm4SJrFNelY+unx6e8PfnOdNbTsUDjOAkDVo +qBpvLYD0/+N0FLPXvi9hxQSPnA0GxAuvJfu1TPGGPDWf3xsqaDC9kDFZ1GOs53qD +iyUzFLpTrm6y/ieg5LrocQbCDnMsTJ6sICpuKVBpT8Xz0ku/jKNyQvevZMIFB3q1 +2QIDAQAB +-----END PUBLIC KEY----- +""" + +x25519_private_key_pem = b"""-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VuBCIEIPAjVfPNTm25VxtBRg+JjjFx9tA3M8aaBdVhjb92iBts +-----END PRIVATE KEY----- +""" + + +@pytest.fixture +def x509_data() -> tuple[PKey, X509]: + """ + Create a new private key and start a certificate request (for a test + to finish in one way or another). + """ + # Basic setup stuff to generate a certificate + pkey = PKey() + pkey.generate_key(TYPE_RSA, 2048) + x509 = X509() + subject = x509.get_subject() + # Authority good you have. + subject.commonName = "Yoda root CA" + x509.set_issuer(subject) + x509.set_pubkey(pkey) + now = datetime.now() + expire = datetime.now() + timedelta(days=100) + x509.set_notBefore(now.strftime("%Y%m%d%H%M%SZ").encode()) + x509.set_notAfter(expire.strftime("%Y%m%d%H%M%SZ").encode()) + return pkey, x509 + + +class TestPKey: + """ + Tests for `OpenSSL.crypto.PKey`. + """ + + @pytest.mark.parametrize( + ("key_string", "key_type"), + [ + (dsa_private_key_pem, dsa.DSAPrivateKey), + (ec_private_key_pem, ec.EllipticCurvePrivateKey), + pytest.param( + ed25519_private_key_pem, + ed25519.Ed25519PrivateKey, + marks=pytest.mark.skipif( + conftest.is_awslc, + reason="aws-lc doesn't support Ed25519 serialization", + ), + ), + pytest.param( + ed448_private_key_pem, + ed448.Ed448PrivateKey, + marks=pytest.mark.skipif( + conftest.is_awslc, + reason="aws-lc doesn't support Ed448", + ), + ), + (rsa_private_key_pem, rsa.RSAPrivateKey), + ], + ) + def test_convert_roundtrip_cryptography_private_key( + self, key_string: bytes, key_type: type[_Key] + ) -> None: + """ + PKey.from_cryptography_key creates a proper private PKey. + PKey.to_cryptography_key creates a proper cryptography private key. + """ + key = serialization.load_pem_private_key(key_string, None) + assert isinstance(key, key_type) + assert isinstance( + key, + ( + dsa.DSAPrivateKey, + ec.EllipticCurvePrivateKey, + ed25519.Ed25519PrivateKey, + ed448.Ed448PrivateKey, + rsa.RSAPrivateKey, + ), + ) + pkey = PKey.from_cryptography_key(key) + + assert isinstance(pkey, PKey) + parsed_key = pkey.to_cryptography_key() + assert isinstance(parsed_key, key_type) + assert isinstance( + parsed_key, + ( + dsa.DSAPrivateKey, + ec.EllipticCurvePrivateKey, + ed25519.Ed25519PrivateKey, + ed448.Ed448PrivateKey, + rsa.RSAPrivateKey, + ), + ) + assert parsed_key.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) == key.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + assert pkey._only_public is False + assert pkey._initialized is True + + @pytest.mark.parametrize( + ("key_string", "key_type"), + [ + (dsa_public_key_pem, dsa.DSAPublicKey), + (ec_public_key_pem, ec.EllipticCurvePublicKey), + pytest.param( + ed25519_public_key_pem, + ed25519.Ed25519PublicKey, + marks=pytest.mark.skipif( + conftest.is_awslc, + reason="aws-lc doesn't support Ed25519 serialization", + ), + ), + pytest.param( + ed448_public_key_pem, + ed448.Ed448PublicKey, + marks=pytest.mark.skipif( + conftest.is_awslc, + reason="aws-lc doesn't support Ed448", + ), + ), + (rsa_public_key_pem, rsa.RSAPublicKey), + ], + ) + def test_convert_roundtrip_cryptography_public_key( + self, key_string: bytes, key_type: type[_Key] + ) -> None: + """ + PKey.from_cryptography_key creates a proper public PKey. + PKey.to_cryptography_key creates a proper cryptography public key. + """ + key = serialization.load_pem_public_key(key_string, None) + assert isinstance(key, key_type) + assert isinstance( + key, + ( + dsa.DSAPublicKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey, + ed448.Ed448PublicKey, + rsa.RSAPublicKey, + ), + ) + pkey = PKey.from_cryptography_key(key) + + assert isinstance(pkey, PKey) + parsed_key = pkey.to_cryptography_key() + assert isinstance(parsed_key, key_type) + assert isinstance( + parsed_key, + ( + dsa.DSAPublicKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey, + ed448.Ed448PublicKey, + rsa.RSAPublicKey, + ), + ) + assert parsed_key.public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) == key.public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + assert pkey._only_public is True + assert pkey._initialized is True + + def test_convert_from_cryptography_public_key(self) -> None: + """ + PKey.from_cryptography_key creates a proper public PKey. + """ + key = serialization.load_pem_public_key(cleartextPublicKeyPEM) + assert isinstance(key, rsa.RSAPublicKey) + pkey = PKey.from_cryptography_key(key) + + assert isinstance(pkey, PKey) + assert pkey.bits() == key.key_size + assert pkey._only_public is True + assert pkey._initialized is True + + def test_convert_from_cryptography_unsupported_type(self) -> None: + """ + PKey.from_cryptography_key raises TypeError with an unsupported type. + """ + key = serialization.load_pem_private_key(x25519_private_key_pem, None) + with pytest.raises(TypeError): + PKey.from_cryptography_key(key) # type: ignore[arg-type] + + def test_convert_public_pkey_to_cryptography_key(self) -> None: + """ + PKey.to_cryptography_key creates a proper cryptography public key. + """ + pkey = load_publickey(FILETYPE_PEM, cleartextPublicKeyPEM) + key = pkey.to_cryptography_key() + + assert isinstance(key, rsa.RSAPublicKey) + assert pkey.bits() == key.key_size + + def test_construction(self) -> None: + """ + `PKey` takes no arguments and returns a new `PKey` instance. + """ + key = PKey() + assert isinstance(key, PKey) + + def test_pregeneration(self) -> None: + """ + `PKey.bits` and `PKey.type` return `0` before the key is generated. + `PKey.check` raises `TypeError` before the key is generated. + """ + key = PKey() + assert key.type() == 0 + assert key.bits() == 0 + with pytest.raises(TypeError): + key.check() + + def test_failed_generation(self) -> None: + """ + `PKey.generate_key` takes two arguments, the first giving the key type + as one of `TYPE_RSA` or `TYPE_DSA` and the second giving the number of + bits to generate. If an invalid type is specified or generation fails, + `Error` is raised. If an invalid number of bits is specified, + `ValueError` or `Error` is raised. + """ + key = PKey() + with pytest.raises(TypeError): + key.generate_key("foo", "bar") # type: ignore[arg-type] + with pytest.raises(Error): + key.generate_key(-1, 0) + + with pytest.raises(ValueError): + key.generate_key(TYPE_RSA, -1) + with pytest.raises(ValueError): + key.generate_key(TYPE_RSA, 0) + + with pytest.raises(TypeError): + key.generate_key(TYPE_RSA, object()) # type: ignore[arg-type] + + def test_rsa_generation(self) -> None: + """ + `PKey.generate_key` generates an RSA key when passed `TYPE_RSA` as a + type and a reasonable number of bits. + """ + bits = 2048 + key = PKey() + key.generate_key(TYPE_RSA, bits) + assert key.type() == TYPE_RSA + assert key.bits() == bits + assert key.check() + + def test_dsa_generation(self) -> None: + """ + `PKey.generate_key` generates a DSA key when passed `TYPE_DSA` as a + type and a reasonable number of bits. + """ + # 512 is a magic number. The DSS (Digital Signature Standard) + # allows a minimum of 512 bits for DSA. DSA_generate_parameters + # will silently promote any value below 512 to 512. + bits = 512 + key = PKey() + key.generate_key(TYPE_DSA, bits) + assert key.type() == TYPE_DSA + assert key.bits() == bits + with pytest.raises(TypeError): + key.check() + + def test_regeneration(self) -> None: + """ + `PKey.generate_key` can be called multiple times on the same key to + generate new keys. + """ + key = PKey() + for type, bits in [(TYPE_RSA, 2048), (TYPE_DSA, 576)]: + key.generate_key(type, bits) + assert key.type() == type + assert key.bits() == bits + + def test_inconsistent_key(self) -> None: + """ + Either `load_privatekey` or `PKey.check` returns `Error` if the key is + not consistent. + """ + with pytest.raises(Error): + key = load_privatekey(FILETYPE_PEM, inconsistentPrivateKeyPEM) + key.check() + + def test_check_public_key(self) -> None: + """ + `PKey.check` raises `TypeError` if only the public part of the key + is available. + """ + # A trick to get a public-only key + key = PKey() + key.generate_key(TYPE_RSA, 2048) + cert = X509() + cert.set_pubkey(key) + pub = cert.get_pubkey() + with pytest.raises(TypeError): + pub.check() + + def test_check_pr_897(self) -> None: + """ + Either `load_privatekey` or `PKey.check` raises `OpenSSL.crypto.Error` + if provided with broken key + """ + with pytest.raises(Error): + pkey = load_privatekey(FILETYPE_PEM, rsa_p_not_prime_pem) + pkey.check() + + +def x509_name(**attrs: str) -> X509Name: + """ + Return a new X509Name with the given attributes. + """ + # XXX There's no other way to get a new X509Name yet. + name = X509().get_subject() + + # Make the order stable - order matters! + def key(attr: tuple[str, str]) -> str: + return attr[1] + + for k, v in sorted(attrs.items(), key=key): + setattr(name, k, v) + return name + + +class TestX509Name: + """ + Unit tests for `OpenSSL.crypto.X509Name`. + """ + + def test_type(self) -> None: + """ + The type of X509Name objects is `X509Name`. + """ + name = x509_name() + assert isinstance(name, X509Name) + + def test_only_string_attributes(self) -> None: + """ + Attempting to set a non-`str` attribute name on an `X509Name` instance + causes `TypeError` to be raised. + """ + name = x509_name() + # Beyond these cases, you may also think that unicode should be + # rejected. Sorry, you're wrong. unicode is automatically converted + # to str outside of the control of X509Name, so there's no way to + # reject it. + + # Also, this used to test str subclasses, but that test is less + # relevant now that the implementation is in Python instead of C. Also + # PyPy automatically converts str subclasses to str when they are + # passed to setattr, so we can't test it on PyPy. Apparently CPython + # does this sometimes as well. + with pytest.raises(TypeError): + setattr(name, None, "hello") # type: ignore[arg-type] + with pytest.raises(TypeError): + setattr(name, 30, "hello") # type: ignore[arg-type] + + def test_set_invalid_attribute(self) -> None: + """ + Attempting to set any attribute name on an `X509Name` instance for + which no corresponding NID is defined causes `AttributeError` to be + raised. + """ + name = x509_name() + with pytest.raises(AttributeError): + setattr(name, "no such thing", None) + + def test_attributes(self) -> None: + """ + `X509Name` instances have attributes for each standard (?) + X509Name field. + """ + name = x509_name() + name.commonName = "foo" + assert name.commonName == "foo" + assert name.CN == "foo" + + name.CN = "baz" + assert name.commonName == "baz" + assert name.CN == "baz" + + name.commonName = "bar" + assert name.commonName == "bar" + assert name.CN == "bar" + + name.CN = "quux" + assert name.commonName == "quux" + assert name.CN == "quux" + + assert name.OU is None + + with pytest.raises(AttributeError): + name.foobar + + def test_copy(self) -> None: + """ + `X509Name` creates a new `X509Name` instance with all the same + attributes as an existing `X509Name` instance when called with one. + """ + name = x509_name(commonName="foo", emailAddress="bar@example.com") + + copy = X509Name(name) + assert copy.commonName == "foo" + assert copy.emailAddress == "bar@example.com" + + # Mutate the copy and ensure the original is unmodified. + copy.commonName = "baz" + assert name.commonName == "foo" + + # Mutate the original and ensure the copy is unmodified. + name.emailAddress = "quux@example.com" + assert copy.emailAddress == "bar@example.com" + + def test_null_bytes_preserved(self) -> None: + """ + Null bytes in X509Name field values are round-tripped correctly. + """ + name = x509_name() + name.CN = "a\x00b" + assert name.CN == "a\x00b" + + def test_repr(self) -> None: + """ + `repr` passed an `X509Name` instance should return a string containing + a description of the type and the NIDs which have been set on it. + """ + name = x509_name(commonName="foo", emailAddress="bar") + assert repr(name) == "" + + def test_comparison(self) -> None: + """ + `X509Name` instances should compare based on their NIDs. + """ + + def _equality( + a: X509Name, + b: object, + assert_true: typing.Callable[[bool], None], + assert_false: typing.Callable[[bool], None], + ) -> None: + assert_true(a == b) + assert_false(a != b) + assert_true(b == a) + assert_false(b != a) + + def assert_true(x: bool) -> None: + assert x + + def assert_false(x: bool) -> None: + assert not x + + def assert_equal(a: X509Name, b: object) -> None: + _equality(a, b, assert_true, assert_false) + + # Instances compare equal to themselves. + name = x509_name() + assert_equal(name, name) + + # Empty instances should compare equal to each other. + assert_equal(x509_name(), x509_name()) + + # Instances with equal NIDs should compare equal to each other. + assert_equal(x509_name(commonName="foo"), x509_name(commonName="foo")) + + # Instance with equal NIDs set using different aliases should compare + # equal to each other. + assert_equal(x509_name(commonName="foo"), x509_name(CN="foo")) + + # Instances with more than one NID with the same values should compare + # equal to each other. + assert_equal( + x509_name(CN="foo", organizationalUnitName="bar"), + x509_name(commonName="foo", OU="bar"), + ) + + def assert_not_equal(a: X509Name, b: object) -> None: + _equality(a, b, assert_false, assert_true) + + # Instances with different values for the same NID should not compare + # equal to each other. + assert_not_equal(x509_name(CN="foo"), x509_name(CN="bar")) + + # Instances with different NIDs should not compare equal to each other. + assert_not_equal(x509_name(CN="foo"), x509_name(OU="foo")) + + assert_not_equal(x509_name(), object()) + + def _inequality( + a: X509Name, + b: X509Name, + assert_true: typing.Callable[[bool], None], + assert_false: typing.Callable[[bool], None], + ) -> None: + assert_true(a < b) + assert_true(a <= b) + assert_true(b > a) + assert_true(b >= a) + assert_false(a > b) + assert_false(a >= b) + assert_false(b < a) + assert_false(b <= a) + + def assert_less_than(a: X509Name, b: X509Name) -> None: + _inequality(a, b, assert_true, assert_false) + + # An X509Name with a NID with a value which sorts less than the value + # of the same NID on another X509Name compares less than the other + # X509Name. + assert_less_than(x509_name(CN="abc"), x509_name(CN="def")) + + def assert_greater_than(a: X509Name, b: X509Name) -> None: + _inequality(a, b, assert_false, assert_true) + + # An X509Name with a NID with a value which sorts greater than the + # value of the same NID on another X509Name compares greater than the + # other X509Name. + assert_greater_than(x509_name(CN="def"), x509_name(CN="abc")) + + def assert_raises(a: X509Name, b: object) -> None: + with pytest.raises(TypeError): + a < b + with pytest.raises(TypeError): + a <= b + with pytest.raises(TypeError): + a > b + with pytest.raises(TypeError): + a >= b + + # Only X509Name objects can be compared with lesser than / greater than + assert_raises(x509_name(), object()) + + def test_hash(self) -> None: + """ + `X509Name.hash` returns an integer hash based on the value of the name. + """ + a = x509_name(CN="foo") + b = x509_name(CN="foo") + assert a.hash() == b.hash() + a.CN = "bar" + assert a.hash() != b.hash() + + def test_der(self) -> None: + """ + `X509Name.der` returns the DER encoded form of the name. + """ + a = x509_name(CN="foo", C="US") + assert ( + a.der() == b"0\x1b1\x0b0\t\x06\x03U\x04\x06\x13\x02US" + b"1\x0c0\n\x06\x03U\x04\x03\x0c\x03foo" + ) + + def test_get_components(self) -> None: + """ + `X509Name.get_components` returns a `list` of two-tuples of `str` + giving the NIDs and associated values which make up the name. + """ + a = x509_name() + assert a.get_components() == [] + a.CN = "foo" + assert a.get_components() == [(b"CN", b"foo")] + a.organizationalUnitName = "bar" + assert a.get_components() == [(b"CN", b"foo"), (b"OU", b"bar")] + + def test_load_nul_byte_attribute(self) -> None: + """ + An `X509Name` from an `X509` instance loaded from a file can have a + NUL byte in the value of one of its attributes. + """ + cert = load_certificate(FILETYPE_PEM, nulbyteSubjectAltNamePEM) + subject = cert.get_subject() + assert "null.python.org\x00example.org" == subject.commonName + + def test_load_nul_byte_components(self) -> None: + """ + An `X509Name` from an `X509` instance loaded from a file can have a + NUL byte in the value of its components + """ + cert = load_certificate(FILETYPE_PEM, nulbyteSubjectAltNamePEM) + subject = cert.get_subject() + components = subject.get_components() + ccn = [value for name, value in components if name == b"CN"] + assert ccn[0] == b"null.python.org\x00example.org" + + def test_set_attribute_failure(self) -> None: + """ + If the value of an attribute cannot be set for some reason then + `Error` is raised. + """ + name = x509_name() + # This value is too long + with pytest.raises(Error): + setattr(name, "O", b"x" * 512) + + +class TestX509: + """ + Tests for `OpenSSL.crypto.X509`. + """ + + pemData = root_cert_pem + root_key_pem + + def test_sign_with_ungenerated(self) -> None: + """ + `X509.sign` raises `ValueError` when passed a `PKey` with no parts. + """ + cert = X509() + key = PKey() + with pytest.raises(ValueError): + cert.sign(key, GOOD_DIGEST) + + def test_sign_with_public_key(self) -> None: + """ + `X509.sign` raises `ValueError` when passed a `PKey` with no private + part as the signing key. + """ + cert = X509() + key = PKey() + key.generate_key(TYPE_RSA, 2048) + cert.set_pubkey(key) + pub = cert.get_pubkey() + with pytest.raises(ValueError): + cert.sign(pub, GOOD_DIGEST) + + def test_sign_with_unknown_digest(self) -> None: + """ + `X509.sign` raises `ValueError` when passed a digest name which is + not known. + """ + cert = X509() + key = PKey() + key.generate_key(TYPE_RSA, 2048) + with pytest.raises(ValueError): + cert.sign(key, BAD_DIGEST) + + def test_sign(self) -> None: + """ + `X509.sign` succeeds when passed a private key object and a + valid digest function. + """ + cert = X509() + key = PKey() + key.generate_key(TYPE_RSA, 2048) + cert.set_pubkey(key) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(24 * 60 * 60) + cert.sign(key, GOOD_DIGEST) + + def test_construction(self) -> None: + """ + `X509` takes no arguments and returns an instance of `X509`. + """ + certificate = X509() + assert isinstance(certificate, X509) + assert type(certificate).__name__ == "X509" + assert type(certificate) is X509 + + def test_set_version_wrong_args(self) -> None: + """ + `X509.set_version` raises `TypeError` if invoked with an argument + not of type `int`. + """ + cert = X509() + with pytest.raises(TypeError): + cert.set_version(None) # type: ignore[arg-type] + + def test_version(self) -> None: + """ + `X509.set_version` sets the certificate version number. + `X509.get_version` retrieves it. + """ + cert = X509() + cert.set_version(2) + assert cert.get_version() == 2 + + def test_serial_number(self) -> None: + """ + The serial number of an `X509` can be retrieved and + modified with `X509.get_serial_number` and + `X509.set_serial_number`. + """ + certificate = X509() + with pytest.raises(TypeError): + certificate.set_serial_number("1") # type: ignore[arg-type] + assert certificate.get_serial_number() == 0 + certificate.set_serial_number(1) + assert certificate.get_serial_number() == 1 + certificate.set_serial_number(2**32 + 1) + assert certificate.get_serial_number() == 2**32 + 1 + certificate.set_serial_number(2**64 + 1) + assert certificate.get_serial_number() == 2**64 + 1 + certificate.set_serial_number(2**128 + 1) + assert certificate.get_serial_number() == 2**128 + 1 + + def _setBoundTest( + self, + get: typing.Callable[[X509], bytes | None], + set: typing.Callable[[X509, bytes], None], + ) -> None: + """ + `X509.set_notBefore` takes a string in the format of an + ASN1 GENERALIZEDTIME and sets the beginning of the certificate's + validity period to it. + """ + certificate = X509() + + # Starts with no value. + assert get(certificate) is None + + # GMT (Or is it UTC?) -exarkun + when = b"20040203040506Z" + set(certificate, when) + assert get(certificate) == when + + if not conftest.is_awslc: + # A plus two hours and thirty minutes offset + when = b"20040203040506+0530" + set(certificate, when) + assert get(certificate) == when + + # A minus one hour fifteen minutes offset + when = b"20040203040506-0115" + set(certificate, when) + assert ( + get( + certificate, + ) + == when + ) + + # An invalid string results in a ValueError + with pytest.raises(ValueError): + set(certificate, b"foo bar") + + def test_set_notBefore(self) -> None: + """ + `X509.set_notBefore` takes a string in the format of an + ASN1 GENERALIZEDTIME and sets the beginning of the certificate's + validity period to it. + """ + self._setBoundTest( + lambda c: c.get_notBefore(), lambda c, v: c.set_notBefore(v) + ) + + def test_set_notAfter(self) -> None: + """ + `X509.set_notAfter` takes a string in the format of an ASN1 + GENERALIZEDTIME and sets the end of the certificate's validity period + to it. + """ + self._setBoundTest( + lambda c: c.get_notAfter(), lambda c, v: c.set_notAfter(v) + ) + + def test_get_notBefore(self) -> None: + """ + `X509.get_notBefore` returns a string in the format of an + ASN1 GENERALIZEDTIME even for certificates which store it as UTCTIME + internally. + """ + cert = load_certificate(FILETYPE_PEM, old_root_cert_pem) + assert cert.get_notBefore() == b"20090325123658Z" + + def test_get_notAfter(self) -> None: + """ + `X509.get_notAfter` returns a string in the format of an + ASN1 GENERALIZEDTIME even for certificates which store it as UTCTIME + internally. + """ + cert = load_certificate(FILETYPE_PEM, old_root_cert_pem) + assert cert.get_notAfter() == b"20170611123658Z" + + def test_gmtime_adj_notBefore_wrong_args(self) -> None: + """ + `X509.gmtime_adj_notBefore` raises `TypeError` if called with a + non-`int` argument. + """ + cert = X509() + with pytest.raises(TypeError): + cert.gmtime_adj_notBefore(None) # type: ignore[arg-type] + + @pytest.mark.flaky(reruns=2) + def test_gmtime_adj_notBefore(self) -> None: + """ + `X509.gmtime_adj_notBefore` changes the not-before timestamp to be the + current time plus the number of seconds passed in. + """ + cert = load_certificate(FILETYPE_PEM, self.pemData) + utc_now = utcnow().replace(microsecond=0) + # -1 second tolerance for clock adjustments + not_before_min = utc_now + timedelta(seconds=99) + cert.gmtime_adj_notBefore(100) + not_before_str = cert.get_notBefore() + assert not_before_str is not None + not_before = datetime.strptime( + not_before_str.decode(), "%Y%m%d%H%M%SZ" + ) + # +1 second tolerance for clock adjustments + not_before_max = utc_now + timedelta(seconds=101) + assert not_before_min <= not_before <= not_before_max + + def test_gmtime_adj_notAfter_wrong_args(self) -> None: + """ + `X509.gmtime_adj_notAfter` raises `TypeError` if called with a + non-`int` argument. + """ + cert = X509() + with pytest.raises(TypeError): + cert.gmtime_adj_notAfter(None) # type: ignore[arg-type] + + @pytest.mark.flaky(reruns=2) + def test_gmtime_adj_notAfter(self) -> None: + """ + `X509.gmtime_adj_notAfter` changes the not-after timestamp + to be the current time plus the number of seconds passed in. + """ + cert = load_certificate(FILETYPE_PEM, self.pemData) + utc_now = utcnow().replace(microsecond=0) + # -1 second tolerance for clock adjustments + not_after_min = utc_now + timedelta(seconds=99) + cert.gmtime_adj_notAfter(100) + not_after_str = cert.get_notAfter() + assert not_after_str is not None + not_after = datetime.strptime(not_after_str.decode(), "%Y%m%d%H%M%SZ") + # +1 second tolerance for clock adjustments + not_after_max = utc_now + timedelta(seconds=101) + assert not_after_min <= not_after <= not_after_max + + def test_has_expired(self) -> None: + """ + `X509.has_expired` returns `True` if the certificate's not-after time + is in the past. + """ + cert = X509() + cert.gmtime_adj_notAfter(-1) + assert cert.has_expired() + + def test_has_not_expired(self) -> None: + """ + `X509.has_expired` returns `False` if the certificate's not-after time + is in the future. + """ + cert = X509() + cert.gmtime_adj_notAfter(2) + assert not cert.has_expired() + + def test_has_expired_exception(self) -> None: + """ + `X509.has_expired` throws ValueError if not-after time is not set + """ + cert = X509() + with pytest.raises(ValueError): + cert.has_expired() + + def test_root_has_not_expired(self) -> None: + """ + `X509.has_expired` returns `False` if the certificate's not-after time + is in the future. + """ + cert = load_certificate(FILETYPE_PEM, root_cert_pem) + assert not cert.has_expired() + + def test_digest(self) -> None: + """ + `X509.digest` returns a string giving ":"-separated hex-encoded + words of the digest of the certificate. + """ + cert = load_certificate(FILETYPE_PEM, old_root_cert_pem) + assert ( + # Digest verified with the command: + # openssl x509 -in root_cert.pem -noout -fingerprint -sha256 + cert.digest("SHA256") + == ( + b"3E:0F:16:39:6B:B1:3E:4F:08:85:C6:5F:10:0D:CB:2C:" + b"25:C2:91:4E:D0:4A:C2:29:06:BD:55:E3:A7:B3:B7:06" + ) + ) + + def _extcert( + self, key: _PrivateKey, extensions: list[x509.ExtensionType] + ) -> X509: + subject = x509.Name( + [x509.NameAttribute(x509.NameOID.COMMON_NAME, "Unit Tests")] + ) + when = datetime.now() + builder = ( + x509.CertificateBuilder() + .public_key(key.public_key()) + .subject_name(subject) + .issuer_name(subject) + .not_valid_before(when) + .not_valid_after(when) + .serial_number(1) + ) + for i, ext in enumerate(extensions): + builder = builder.add_extension(ext, critical=i % 2 == 0) + + return X509.from_cryptography(builder.sign(key, hashes.SHA256())) + + def test_extension_count(self) -> None: + """ + `X509.get_extension_count` returns the number of extensions + that are present in the certificate. + """ + pkey = load_privatekey( + FILETYPE_PEM, client_key_pem + ).to_cryptography_key() + assert isinstance(pkey, rsa.RSAPrivateKey) + ca = x509.BasicConstraints(ca=False, path_length=None) + key = x509.KeyUsage( + digital_signature=True, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ) + san = x509.SubjectAlternativeName([x509.DNSName("example.com")]) + + # Try a certificate with no extensions at all. + c = self._extcert(pkey, []) + assert c.get_extension_count() == 0 + + # And a certificate with one + c = self._extcert(pkey, [ca]) + assert c.get_extension_count() == 1 + + # And a certificate with several + c = self._extcert(pkey, [ca, key, san]) + assert c.get_extension_count() == 3 + + def test_invalid_digest_algorithm(self) -> None: + """ + `X509.digest` raises `ValueError` if called with an unrecognized hash + algorithm. + """ + cert = X509() + with pytest.raises(ValueError): + cert.digest(BAD_DIGEST) + + def test_get_subject(self) -> None: + """ + `X509.get_subject` returns an `X509Name` instance. + """ + cert = load_certificate(FILETYPE_PEM, self.pemData) + subj = cert.get_subject() + assert isinstance(subj, X509Name) + assert subj.get_components() == [ + (b"C", b"US"), + (b"ST", b"IL"), + (b"L", b"Chicago"), + (b"O", b"Testing"), + (b"CN", b"Testing Root CA"), + ] + + def test_set_subject_wrong_args(self) -> None: + """ + `X509.set_subject` raises a `TypeError` if called with an argument not + of type `X509Name`. + """ + cert = X509() + with pytest.raises(TypeError): + cert.set_subject(None) # type: ignore[arg-type] + + def test_set_subject(self) -> None: + """ + `X509.set_subject` changes the subject of the certificate to the one + passed in. + """ + cert = X509() + name = cert.get_subject() + name.C = "AU" + name.OU = "Unit Tests" + cert.set_subject(name) + assert cert.get_subject().get_components() == [ + (b"C", b"AU"), + (b"OU", b"Unit Tests"), + ] + + def test_get_issuer(self) -> None: + """ + `X509.get_issuer` returns an `X509Name` instance. + """ + cert = load_certificate(FILETYPE_PEM, self.pemData) + subj = cert.get_issuer() + assert isinstance(subj, X509Name) + comp = subj.get_components() + assert comp == [ + (b"C", b"US"), + (b"ST", b"IL"), + (b"L", b"Chicago"), + (b"O", b"Testing"), + (b"CN", b"Testing Root CA"), + ] + + def test_set_issuer_wrong_args(self) -> None: + """ + `X509.set_issuer` raises a `TypeError` if called with an argument not + of type `X509Name`. + """ + cert = X509() + with pytest.raises(TypeError): + cert.set_issuer(None) # type: ignore[arg-type] + + def test_set_issuer(self) -> None: + """ + `X509.set_issuer` changes the issuer of the certificate to the + one passed in. + """ + cert = X509() + name = cert.get_issuer() + name.C = "AU" + name.OU = "Unit Tests" + cert.set_issuer(name) + assert cert.get_issuer().get_components() == [ + (b"C", b"AU"), + (b"OU", b"Unit Tests"), + ] + + def test_get_pubkey_uninitialized(self) -> None: + """ + When called on a certificate with no public key, `X509.get_pubkey` + raises `OpenSSL.crypto.Error`. + """ + cert = X509() + with pytest.raises(Error): + cert.get_pubkey() + + def test_set_pubkey_wrong_type(self) -> None: + """ + `X509.set_pubkey` raises `TypeError` when given an object of the + wrong type. + """ + cert = X509() + with pytest.raises(TypeError): + cert.set_pubkey(object()) # type: ignore[arg-type] + + def test_subject_name_hash(self) -> None: + """ + `X509.subject_name_hash` returns the hash of the certificate's + subject name. + """ + cert = load_certificate(FILETYPE_PEM, self.pemData) + # SHA1 + assert cert.subject_name_hash() == 3278919224 + + def test_get_signature_algorithm(self) -> None: + """ + `X509.get_signature_algorithm` returns a string which means + the algorithm used to sign the certificate. + """ + cert = load_certificate(FILETYPE_PEM, self.pemData) + assert b"sha256WithRSAEncryption" == cert.get_signature_algorithm() + + def test_get_undefined_signature_algorithm(self) -> None: + """ + `X509.get_signature_algorithm` raises `ValueError` if the signature + algorithm is undefined or unknown. + """ + # This certificate has been modified to indicate a bogus OID in the + # signature algorithm field so that OpenSSL does not recognize it. + certPEM = b"""\ +-----BEGIN CERTIFICATE----- +MIIC/zCCAmigAwIBAgIBATAGBgJ8BQUAMHsxCzAJBgNVBAYTAlNHMREwDwYDVQQK +EwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0ExJDAiBgNVBAMTG00yQ3J5 +cHRvIENlcnRpZmljYXRlIE1hc3RlcjEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0 +MS5jb20wHhcNMDAwOTEwMDk1MTMwWhcNMDIwOTEwMDk1MTMwWjBTMQswCQYDVQQG +EwJTRzERMA8GA1UEChMITTJDcnlwdG8xEjAQBgNVBAMTCWxvY2FsaG9zdDEdMBsG +CSqGSIb3DQEJARYObmdwc0Bwb3N0MS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBI +AkEArL57d26W9fNXvOhNlZzlPOACmvwOZ5AdNgLzJ1/MfsQQJ7hHVeHmTAjM664V ++fXvwUGJLziCeBo1ysWLRnl8CQIDAQABo4IBBDCCAQAwCQYDVR0TBAIwADAsBglg +hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O +BBYEFM+EgpK+eyZiwFU1aOPSbczbPSpVMIGlBgNVHSMEgZ0wgZqAFPuHI2nrnDqT +FeXFvylRT/7tKDgBoX+kfTB7MQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlw +dG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQDExtNMkNyeXB0byBDZXJ0 +aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tggEA +MA0GCSqGSIb3DQEBBAUAA4GBADv8KpPo+gfJxN2ERK1Y1l17sz/ZhzoGgm5XCdbx +jEY7xKfpQngV599k1xhl11IMqizDwu0855agrckg2MCTmOI9DZzDD77tAYb+Dk0O +PEVk0Mk/V0aIsDE9bolfCi/i/QWZ3N8s5nTWMNyBBBmoSliWCm4jkkRZRD0ejgTN +tgI5 +-----END CERTIFICATE----- +""" + cert = load_certificate(FILETYPE_PEM, certPEM) + with pytest.raises(ValueError): + cert.get_signature_algorithm() + + def test_sign_bad_pubkey_type(self) -> None: + """ + `X509.sign` raises `TypeError` when called with the wrong type. + """ + cert = X509() + with pytest.raises(TypeError): + cert.sign(object(), b"sha256") # type: ignore[arg-type] + + def test_convert_from_cryptography(self) -> None: + crypto_cert = x509.load_pem_x509_certificate(intermediate_cert_pem) + cert = X509.from_cryptography(crypto_cert) + + assert isinstance(cert, X509) + assert cert.get_version() == crypto_cert.version.value + + def test_convert_from_cryptography_unsupported_type(self) -> None: + with pytest.raises(TypeError): + X509.from_cryptography(object()) # type: ignore[arg-type] + + def test_convert_to_cryptography_key(self) -> None: + cert = load_certificate(FILETYPE_PEM, intermediate_cert_pem) + crypto_cert = cert.to_cryptography() + + assert isinstance(crypto_cert, x509.Certificate) + assert crypto_cert.version.value == cert.get_version() + + +class TestX509Store: + """ + Test for `OpenSSL.crypto.X509Store`. + """ + + def test_add_cert(self) -> None: + """ + `X509Store.add_cert` adds a `X509` instance to the certificate store. + """ + cert = load_certificate(FILETYPE_PEM, root_cert_pem) + store = X509Store() + store.add_cert(cert) + + @pytest.mark.parametrize("cert", [None, 1.0, "cert", object()]) + def test_add_cert_wrong_args(self, cert: object) -> None: + """ + `X509Store.add_cert` raises `TypeError` if passed a non-X509 object + as its first argument. + """ + store = X509Store() + with pytest.raises(TypeError): + store.add_cert(cert) # type: ignore[arg-type] + + def test_add_cert_accepts_duplicate(self) -> None: + """ + `X509Store.add_cert` doesn't raise `OpenSSL.crypto.Error` if an attempt + is made to add the same certificate to the store more than once. + """ + cert = load_certificate(FILETYPE_PEM, root_cert_pem) + store = X509Store() + store.add_cert(cert) + store.add_cert(cert) + + @pytest.mark.parametrize( + "cafile, capath, call_cafile, call_capath", + [ + ( + "/cafile" + NON_ASCII, + None, + b"/cafile" + NON_ASCII.encode(sys.getfilesystemencoding()), + _ffi.NULL, + ), + ( + b"/cafile" + NON_ASCII.encode("utf-8"), + None, + b"/cafile" + NON_ASCII.encode("utf-8"), + _ffi.NULL, + ), + ( + None, + "/capath" + NON_ASCII, + _ffi.NULL, + b"/capath" + NON_ASCII.encode(sys.getfilesystemencoding()), + ), + ( + None, + b"/capath" + NON_ASCII.encode("utf-8"), + _ffi.NULL, + b"/capath" + NON_ASCII.encode("utf-8"), + ), + ], + ) + def test_load_locations_parameters( + self, + cafile: str | bytes | None, + capath: str | bytes | None, + call_cafile: object, + call_capath: object, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + class LibMock: + def load_locations( + self, store: object, cafile: object, capath: object + ) -> int: + self.cafile = cafile + self.capath = capath + return 1 + + lib_mock = LibMock() + monkeypatch.setattr( + _lib, "X509_STORE_load_locations", lib_mock.load_locations + ) + + store = X509Store() + store.load_locations(cafile=cafile, capath=capath) + + assert call_cafile == lib_mock.cafile + assert call_capath == lib_mock.capath + + def test_load_locations_fails_when_all_args_are_none(self) -> None: + store = X509Store() + with pytest.raises(Error): + store.load_locations(None, None) + + def test_load_locations_raises_error_on_failure( + self, tmp_path: pathlib.Path + ) -> None: + invalid_ca_file = tmp_path / "invalid.pem" + invalid_ca_file.write_text("This is not a certificate") + + store = X509Store() + with pytest.raises(Error): + store.load_locations(cafile=str(invalid_ca_file)) + + +def _runopenssl(pem: bytes, *args: bytes) -> bytes: + """ + Run the command line openssl tool with the given arguments and write + the given PEM to its stdin. Not safe for quotes. + """ + proc = Popen([b"openssl", *list(args)], stdin=PIPE, stdout=PIPE) + assert proc.stdin is not None + assert proc.stdout is not None + proc.stdin.write(pem) + proc.stdin.close() + output = proc.stdout.read() + proc.stdout.close() + proc.wait() + return output + + +class TestLoadPublicKey: + """ + Tests for :func:`load_publickey`. + """ + + def test_loading_works(self) -> None: + """ + load_publickey loads public keys and sets correct attributes. + """ + key = load_publickey(FILETYPE_PEM, cleartextPublicKeyPEM) + + assert True is key._only_public + assert 2048 == key.bits() + assert TYPE_RSA == key.type() + + def test_invalid_type(self) -> None: + """ + load_publickey doesn't support FILETYPE_TEXT. + """ + with pytest.raises(ValueError): + load_publickey(FILETYPE_TEXT, cleartextPublicKeyPEM) + + def test_invalid_key_format(self) -> None: + """ + load_publickey explodes on incorrect keys. + """ + with pytest.raises(Error): + load_publickey(FILETYPE_ASN1, cleartextPublicKeyPEM) + + def test_tolerates_unicode_strings(self) -> None: + """ + load_publickey works with text strings, not just bytes. + """ + serialized = cleartextPublicKeyPEM.decode("ascii") + key = load_publickey(FILETYPE_PEM, serialized) + dumped_pem = dump_publickey(FILETYPE_PEM, key) + + assert dumped_pem == cleartextPublicKeyPEM + + +class TestFunction: + """ + Tests for free-functions in the `OpenSSL.crypto` module. + """ + + def test_load_privatekey_invalid_format(self) -> None: + """ + `load_privatekey` raises `ValueError` if passed an unknown filetype. + """ + with pytest.raises(ValueError): + load_privatekey(100, root_key_pem) + + def test_load_privatekey_invalid_passphrase_type(self) -> None: + """ + `load_privatekey` raises `TypeError` if passed a passphrase that is + neither a `str` nor a callable. + """ + with pytest.raises(TypeError): + load_privatekey( + FILETYPE_PEM, + encryptedPrivateKeyPEMPassphrase, + object(), # type: ignore[arg-type] + ) + + def test_load_privatekey_wrongPassphrase(self) -> None: + """ + `load_privatekey` raises `OpenSSL.crypto.Error` when it is passed an + encrypted PEM and an incorrect passphrase. + """ + with pytest.raises(Error) as err: + load_privatekey(FILETYPE_PEM, encryptedPrivateKeyPEM, b"quack") + assert err.value.args[0] != [] + + def test_load_privatekey_passphraseWrongType(self) -> None: + """ + `load_privatekey` raises `ValueError` when it is passed a passphrase + with a private key encoded in a format, that doesn't support + encryption. + """ + key = load_privatekey(FILETYPE_PEM, root_key_pem) + blob = dump_privatekey(FILETYPE_ASN1, key) + with pytest.raises(ValueError): + load_privatekey(FILETYPE_ASN1, blob, b"secret") + + def test_load_privatekey_passphrase(self) -> None: + """ + `load_privatekey` can create a `PKey` object from an encrypted PEM + string if given the passphrase. + """ + key = load_privatekey( + FILETYPE_PEM, + encryptedPrivateKeyPEM, + encryptedPrivateKeyPEMPassphrase, + ) + assert isinstance(key, PKey) + + def test_load_privatekey_passphrase_exception(self) -> None: + """ + If the passphrase callback raises an exception, that exception is + raised by `load_privatekey`. + """ + + def cb(ignored: object) -> bytes: + raise ArithmeticError + + with pytest.raises(ArithmeticError): + load_privatekey(FILETYPE_PEM, encryptedPrivateKeyPEM, cb) + + def test_load_privatekey_wrongPassphraseCallback(self) -> None: + """ + `load_privatekey` raises `OpenSSL.crypto.Error` when it + is passed an encrypted PEM and a passphrase callback which returns an + incorrect passphrase. + """ + called = False + + def cb(*a: object) -> bytes: + nonlocal called + called = True + return b"quack" + + with pytest.raises(Error) as err: + load_privatekey(FILETYPE_PEM, encryptedPrivateKeyPEM, cb) + assert called + assert err.value.args[0] != [] + + def test_load_privatekey_passphraseCallback(self) -> None: + """ + `load_privatekey` can create a `PKey` object from an encrypted PEM + string if given a passphrase callback which returns the correct + password. + """ + called = [] + + def cb(writing: bool) -> bytes: + called.append(writing) + return encryptedPrivateKeyPEMPassphrase + + key = load_privatekey(FILETYPE_PEM, encryptedPrivateKeyPEM, cb) + assert isinstance(key, PKey) + assert called == [False] + + def test_load_privatekey_passphrase_wrong_return_type(self) -> None: + """ + `load_privatekey` raises `ValueError` if the passphrase callback + returns something other than a byte string. + """ + with pytest.raises(ValueError): + load_privatekey( + FILETYPE_PEM, + encryptedPrivateKeyPEM, + lambda *args: 3, # type: ignore[arg-type] + ) + + def test_dump_privatekey_wrong_args(self) -> None: + """ + `dump_privatekey` raises `TypeError` if called with a `cipher` + argument but no `passphrase` argument. + """ + key = PKey() + key.generate_key(TYPE_RSA, 2048) + with pytest.raises(TypeError): + dump_privatekey(FILETYPE_PEM, key, cipher=GOOD_CIPHER) + + def test_dump_privatekey_not_rsa_key(self) -> None: + """ + `dump_privatekey` raises `TypeError` if called with a key that is + not RSA. + """ + key = PKey() + key.generate_key(TYPE_DSA, 512) + with pytest.raises(TypeError): + dump_privatekey(FILETYPE_TEXT, key) + + def test_dump_privatekey_invalid_pkey(self) -> None: + with pytest.raises(TypeError): + dump_privatekey(FILETYPE_TEXT, object()) # type: ignore[arg-type] + + def test_dump_privatekey_unknown_cipher(self) -> None: + """ + `dump_privatekey` raises `ValueError` if called with an unrecognized + cipher name. + """ + key = PKey() + key.generate_key(TYPE_RSA, 2048) + with pytest.raises(ValueError): + dump_privatekey(FILETYPE_PEM, key, BAD_CIPHER, b"passphrase") + + def test_dump_privatekey_invalid_passphrase_type(self) -> None: + """ + `dump_privatekey` raises `TypeError` if called with a passphrase which + is neither a `str` nor a callable. + """ + key = PKey() + key.generate_key(TYPE_RSA, 2048) + with pytest.raises(TypeError): + dump_privatekey(FILETYPE_PEM, key, GOOD_CIPHER, object()) # type: ignore[arg-type] + + def test_dump_privatekey_invalid_filetype(self) -> None: + """ + `dump_privatekey` raises `ValueError` if called with an unrecognized + filetype. + """ + key = PKey() + key.generate_key(TYPE_RSA, 2048) + with pytest.raises(ValueError): + dump_privatekey(100, key) + + def test_load_privatekey_passphrase_callback_length(self) -> None: + """ + `crypto.load_privatekey` should raise an error when the passphrase + provided by the callback is too long, not silently truncate it. + """ + + def cb(ignored: object) -> bytes: + return b"a" * 1025 + + with pytest.raises(ValueError): + load_privatekey(FILETYPE_PEM, encryptedPrivateKeyPEM, cb) + + def test_dump_privatekey_passphrase(self) -> None: + """ + `dump_privatekey` writes an encrypted PEM when given a passphrase. + """ + passphrase = b"foo" + key = load_privatekey(FILETYPE_PEM, root_key_pem) + pem = dump_privatekey(FILETYPE_PEM, key, GOOD_CIPHER, passphrase) + assert isinstance(pem, bytes) + loadedKey = load_privatekey(FILETYPE_PEM, pem, passphrase) + assert isinstance(loadedKey, PKey) + assert loadedKey.type() == key.type() + assert loadedKey.bits() == key.bits() + + def test_dump_privatekey_passphrase_wrong_type(self) -> None: + """ + `dump_privatekey` raises `ValueError` when it is passed a passphrase + with a private key encoded in a format, that doesn't support + encryption. + """ + key = load_privatekey(FILETYPE_PEM, root_key_pem) + with pytest.raises(ValueError): + dump_privatekey(FILETYPE_ASN1, key, GOOD_CIPHER, b"secret") + + def test_dump_certificate(self) -> None: + """ + `dump_certificate` writes PEM, DER, and text. + """ + pemData = root_cert_pem + root_key_pem + cert = load_certificate(FILETYPE_PEM, pemData) + dumped_pem = dump_certificate(FILETYPE_PEM, cert) + assert dumped_pem == root_cert_pem + dumped_der = dump_certificate(FILETYPE_ASN1, cert) + good_der = _runopenssl(dumped_pem, b"x509", b"-outform", b"DER") + assert dumped_der == good_der + cert2 = load_certificate(FILETYPE_ASN1, dumped_der) + dumped_pem2 = dump_certificate(FILETYPE_PEM, cert2) + assert dumped_pem2 == root_cert_pem + dumped_text = dump_certificate(FILETYPE_TEXT, cert) + assert len(dumped_text) > 500 + + def test_dump_certificate_bad_type(self) -> None: + """ + `dump_certificate` raises a `ValueError` if it's called with + a bad type. + """ + cert = load_certificate(FILETYPE_PEM, root_cert_pem) + with pytest.raises(ValueError): + dump_certificate(object(), cert) # type: ignore[arg-type] + + def test_dump_privatekey_pem(self) -> None: + """ + `dump_privatekey` writes a PEM + """ + key = load_privatekey(FILETYPE_PEM, root_key_pem) + assert key.check() + dumped_pem = dump_privatekey(FILETYPE_PEM, key) + assert dumped_pem == normalized_root_key_pem + + def test_dump_privatekey_asn1(self) -> None: + """ + `dump_privatekey` writes a DER + """ + key = load_privatekey(FILETYPE_PEM, root_key_pem) + + dumped_der = dump_privatekey(FILETYPE_ASN1, key) + assert dumped_der == root_key_der + + def test_load_privatekey_asn1(self) -> None: + """ + `dump_privatekey` writes a DER + """ + key = load_privatekey(FILETYPE_ASN1, root_key_der) + assert key.bits() == 3072 + assert key.type() == TYPE_RSA + + def test_dump_privatekey_text(self) -> None: + """ + `dump_privatekey` writes a text + """ + key = load_privatekey(FILETYPE_PEM, root_key_pem) + dumped_text = dump_privatekey(FILETYPE_TEXT, key) + assert len(dumped_text) > 500 + + def test_dump_publickey_pem(self) -> None: + """ + dump_publickey writes a PEM. + """ + key = load_publickey(FILETYPE_PEM, cleartextPublicKeyPEM) + dumped_pem = dump_publickey(FILETYPE_PEM, key) + assert dumped_pem == cleartextPublicKeyPEM + + def test_dump_publickey_asn1(self) -> None: + """ + dump_publickey writes a DER. + """ + key = load_publickey(FILETYPE_PEM, cleartextPublicKeyPEM) + dumped_der = dump_publickey(FILETYPE_ASN1, key) + key2 = load_publickey(FILETYPE_ASN1, dumped_der) + dumped_pem2 = dump_publickey(FILETYPE_PEM, key2) + assert dumped_pem2 == cleartextPublicKeyPEM + + def test_dump_publickey_invalid_type(self) -> None: + """ + dump_publickey doesn't support FILETYPE_TEXT. + """ + key = load_publickey(FILETYPE_PEM, cleartextPublicKeyPEM) + + with pytest.raises(ValueError): + dump_publickey(FILETYPE_TEXT, key) + + def test_dump_privatekey_passphrase_callback(self) -> None: + """ + `dump_privatekey` writes an encrypted PEM when given a callback + which returns the correct passphrase. + """ + passphrase = b"foo" + called = [] + + def cb(writing: bool) -> bytes: + called.append(writing) + return passphrase + + key = load_privatekey(FILETYPE_PEM, root_key_pem) + pem = dump_privatekey(FILETYPE_PEM, key, GOOD_CIPHER, cb) + assert isinstance(pem, bytes) + assert called == [True] + loadedKey = load_privatekey(FILETYPE_PEM, pem, passphrase) + assert isinstance(loadedKey, PKey) + assert loadedKey.type() == key.type() + assert loadedKey.bits() == key.bits() + + def test_dump_privatekey_passphrase_exception(self) -> None: + """ + `dump_privatekey` should not overwrite the exception raised + by the passphrase callback. + """ + + def cb(ignored: object) -> bytes: + raise ArithmeticError + + key = load_privatekey(FILETYPE_PEM, root_key_pem) + with pytest.raises(ArithmeticError): + dump_privatekey(FILETYPE_PEM, key, GOOD_CIPHER, cb) + + def test_dump_privatekey_passphraseCallbackLength(self) -> None: + """ + `crypto.dump_privatekey` should raise an error when the passphrase + provided by the callback is too long, not silently truncate it. + """ + + def cb(ignored: object) -> bytes: + return b"a" * 1025 + + key = load_privatekey(FILETYPE_PEM, root_key_pem) + with pytest.raises(ValueError): + dump_privatekey(FILETYPE_PEM, key, GOOD_CIPHER, cb) + + def test_dump_privatekey_truncated(self) -> None: + """ + `crypto.dump_privatekey` should not truncate a passphrase that contains + a null byte. + """ + key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) + passphrase = b"foo\x00bar" + truncated_passphrase = passphrase.split(b"\x00", 1)[0] + + # By dumping with the full passphrase load should raise an error if we + # try to load using the truncated passphrase. If dump truncated the + # passphrase, then we WILL load the privatekey and the test fails + encrypted_key_pem = dump_privatekey( + FILETYPE_PEM, key, "AES-256-CBC", passphrase + ) + with pytest.raises(Error): + load_privatekey( + FILETYPE_PEM, encrypted_key_pem, truncated_passphrase + ) + + def test_load_privatekey_truncated(self) -> None: + """ + `crypto.load_privatekey` should not truncate a passphrase that contains + a null byte. + """ + key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM) + passphrase = b"foo\x00bar" + truncated_passphrase = passphrase.split(b"\x00", 1)[0] + + # By dumping using the truncated passphrase load should raise an error + # if we try to load using the full passphrase. If load truncated the + # passphrase, then we WILL load the privatekey and the test fails + encrypted_key_pem = dump_privatekey( + FILETYPE_PEM, key, "AES-256-CBC", truncated_passphrase + ) + with pytest.raises(Error): + load_privatekey(FILETYPE_PEM, encrypted_key_pem, passphrase) + + +class TestLoadCertificate: + """ + Tests for `load_certificate`. + """ + + def test_bad_file_type(self) -> None: + """ + If the file type passed to `load_certificate` is neither + `FILETYPE_PEM` nor `FILETYPE_ASN1` then `ValueError` is raised. + """ + with pytest.raises(ValueError): + load_certificate(object(), b"") # type: ignore[arg-type] + + def test_bad_certificate(self) -> None: + """ + If the bytes passed to `load_certificate` are not a valid certificate, + an exception is raised. + """ + with pytest.raises(Error): + load_certificate(FILETYPE_ASN1, b"lol") + + +class TestCRL: + """ + Tests for `OpenSSL.crypto.CRL`. + """ + + cert = load_certificate(FILETYPE_PEM, root_cert_pem) + pkey = load_privatekey(FILETYPE_PEM, root_key_pem) + + root_cert = load_certificate(FILETYPE_PEM, root_cert_pem) + root_key = load_privatekey(FILETYPE_PEM, root_key_pem) + intermediate_cert = load_certificate(FILETYPE_PEM, intermediate_cert_pem) + intermediate_key = load_privatekey(FILETYPE_PEM, intermediate_key_pem) + intermediate_server_cert = load_certificate( + FILETYPE_PEM, intermediate_server_cert_pem + ) + intermediate_server_key = load_privatekey( + FILETYPE_PEM, intermediate_server_key_pem + ) + + @staticmethod + def _make_test_crl_cryptography( + issuer_cert: X509, issuer_key: PKey, certs: list[X509] = [] + ) -> x509.CertificateRevocationList: + """ + Create a CRL using cryptography's API. + + :param list[X509] certs: A list of certificates to revoke. + :rtype: ``cryptography.x509.CertificateRevocationList`` + """ + from cryptography.x509.extensions import CRLReason, ReasonFlags + + builder = x509.CertificateRevocationListBuilder() + builder = builder.issuer_name(issuer_cert.to_cryptography().subject) + for cert in certs: + revoked = ( + x509.RevokedCertificateBuilder() + .serial_number(cert.get_serial_number()) + .revocation_date(datetime(2014, 6, 1, 0, 0, 0)) + .add_extension(CRLReason(ReasonFlags.unspecified), False) + .build() + ) + builder = builder.add_revoked_certificate(revoked) + + builder = builder.last_update(datetime(2014, 6, 1, 0, 0, 0)) + # The year 5000 is far into the future so that this CRL isn't + # considered to have expired. + builder = builder.next_update(datetime(5000, 6, 1, 0, 0, 0)) + + key = issuer_key.to_cryptography_key() + assert isinstance(key, rsa.RSAPrivateKey) + crl = builder.sign( + key, + algorithm=hashes.SHA512(), + ) + return crl + + def test_verify_with_revoked(self) -> None: + """ + `verify_certificate` raises error when an intermediate certificate is + revoked. + """ + store = X509Store() + store.add_cert(self.root_cert) + store.add_cert(self.intermediate_cert) + root_crl = self._make_test_crl_cryptography( + self.root_cert, self.root_key, certs=[self.intermediate_cert] + ) + intermediate_crl = self._make_test_crl_cryptography( + self.intermediate_cert, self.intermediate_key, certs=[] + ) + store.add_crl(root_crl) + store.add_crl(intermediate_crl) + store.set_flags( + X509StoreFlags.CRL_CHECK | X509StoreFlags.CRL_CHECK_ALL + ) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + with pytest.raises(X509StoreContextError) as err: + store_ctx.verify_certificate() + assert str(err.value) == "certificate revoked" + + def test_verify_with_missing_crl(self) -> None: + """ + `verify_certificate` raises error when an intermediate certificate's + CRL is missing. + """ + store = X509Store() + store.add_cert(self.root_cert) + store.add_cert(self.intermediate_cert) + root_crl = self._make_test_crl_cryptography( + self.root_cert, self.root_key, certs=[self.intermediate_cert] + ) + store.add_crl(root_crl) + store.set_flags( + X509StoreFlags.CRL_CHECK | X509StoreFlags.CRL_CHECK_ALL + ) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + with pytest.raises(X509StoreContextError) as err: + store_ctx.verify_certificate() + assert str(err.value) == "unable to get certificate CRL" + assert err.value.certificate.get_subject().CN == "intermediate-service" + + +class TestX509StoreContext: + """ + Tests for `OpenSSL.crypto.X509StoreContext`. + """ + + root_cert = load_certificate(FILETYPE_PEM, root_cert_pem) + intermediate_cert = load_certificate(FILETYPE_PEM, intermediate_cert_pem) + intermediate_server_cert = load_certificate( + FILETYPE_PEM, intermediate_server_cert_pem + ) + + def test_valid(self) -> None: + """ + `verify_certificate` returns ``None`` when called with a certificate + and valid chain. + """ + store = X509Store() + store.add_cert(self.root_cert) + store.add_cert(self.intermediate_cert) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + assert store_ctx.verify_certificate() is None # type: ignore[func-returns-value] + + def test_reuse(self) -> None: + """ + `verify_certificate` can be called multiple times with the same + ``X509StoreContext`` instance to produce the same result. + """ + store = X509Store() + store.add_cert(self.root_cert) + store.add_cert(self.intermediate_cert) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + assert store_ctx.verify_certificate() is None # type: ignore[func-returns-value] + assert store_ctx.verify_certificate() is None # type: ignore[func-returns-value] + + @pytest.mark.parametrize( + "root_cert, chain, verified_cert", + [ + pytest.param( + root_cert, + [intermediate_cert], + intermediate_server_cert, + id="intermediate in chain", + ), + pytest.param( + root_cert, + [], + intermediate_cert, + id="empty chain", + ), + pytest.param( + root_cert, + [root_cert, intermediate_server_cert, intermediate_cert], + intermediate_server_cert, + id="extra certs in chain", + ), + ], + ) + def test_verify_success_with_chain( + self, root_cert: X509, chain: list[X509], verified_cert: X509 + ) -> None: + store = X509Store() + store.add_cert(root_cert) + store_ctx = X509StoreContext(store, verified_cert, chain=chain) + assert store_ctx.verify_certificate() is None # type: ignore[func-returns-value] + + def test_valid_untrusted_chain_reuse(self) -> None: + """ + `verify_certificate` using an untrusted chain can be called multiple + times with the same ``X509StoreContext`` instance to produce the same + result. + """ + store = X509Store() + store.add_cert(self.root_cert) + chain = [self.intermediate_cert] + + store_ctx = X509StoreContext( + store, self.intermediate_server_cert, chain=chain + ) + assert store_ctx.verify_certificate() is None # type: ignore[func-returns-value] + assert store_ctx.verify_certificate() is None # type: ignore[func-returns-value] + + def test_chain_reference(self) -> None: + """ + ``X509StoreContext`` properly keeps references to the untrusted chain + certificates. + """ + store = X509Store() + store.add_cert(self.root_cert) + chain = [load_certificate(FILETYPE_PEM, intermediate_cert_pem)] + + store_ctx = X509StoreContext( + store, self.intermediate_server_cert, chain=chain + ) + + del chain + assert store_ctx.verify_certificate() is None # type: ignore[func-returns-value] + + @pytest.mark.parametrize( + "root_cert, chain, verified_cert", + [ + pytest.param( + root_cert, + [], + intermediate_server_cert, + id="intermediate missing", + ), + pytest.param( + None, + [intermediate_cert], + intermediate_server_cert, + id="no trusted root", + ), + pytest.param( + None, + [root_cert, intermediate_cert], + intermediate_server_cert, + id="untrusted root, full chain is available", + ), + pytest.param( + intermediate_cert, + [root_cert, intermediate_cert], + intermediate_server_cert, + id="untrusted root, intermediate is trusted and in chain", + ), + ], + ) + def test_verify_fail_with_chain( + self, root_cert: X509, chain: list[X509], verified_cert: X509 + ) -> None: + store = X509Store() + if root_cert: + store.add_cert(root_cert) + + store_ctx = X509StoreContext(store, verified_cert, chain=chain) + + with pytest.raises(X509StoreContextError): + store_ctx.verify_certificate() + + @pytest.mark.parametrize( + "chain, expected_error", + [ + pytest.param( + [intermediate_cert, "This is not a certificate"], + TypeError, + id="non-certificate in chain", + ), + pytest.param( + 42, + TypeError, + id="non-list chain", + ), + ], + ) + def test_untrusted_chain_wrong_args( + self, chain: list[X509], expected_error: type[Exception] + ) -> None: + """ + Creating ``X509StoreContext`` with wrong chain raises an exception. + """ + store = X509Store() + store.add_cert(self.root_cert) + + with pytest.raises(expected_error): + X509StoreContext(store, self.intermediate_server_cert, chain=chain) + + def test_failure_building_untrusted_chain_raises( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """ + Creating ``X509StoreContext`` raises ``OpenSSL.crypto.Error`` when + the underlying lib fails to add the certificate to the stack. + """ + monkeypatch.setattr(_lib, "sk_X509_push", lambda _stack, _x509: -1) + + store = X509Store() + store.add_cert(self.root_cert) + chain = [self.intermediate_cert] + + with pytest.raises(Error): + X509StoreContext(store, self.intermediate_server_cert, chain=chain) + + def test_trusted_self_signed(self) -> None: + """ + `verify_certificate` returns ``None`` when called with a self-signed + certificate and itself in the chain. + """ + store = X509Store() + store.add_cert(self.root_cert) + store_ctx = X509StoreContext(store, self.root_cert) + assert store_ctx.verify_certificate() is None # type: ignore[func-returns-value] + + def test_untrusted_self_signed(self) -> None: + """ + `verify_certificate` raises error when a self-signed certificate is + verified without itself in the chain. + """ + store = X509Store() + store_ctx = X509StoreContext(store, self.root_cert) + with pytest.raises(X509StoreContextError) as exc: + store_ctx.verify_certificate() + + # OpenSSL 1.1.x and 3.0.x have different error messages + assert str(exc.value) in [ + "self signed certificate", + "self-signed certificate", + ] + assert exc.value.certificate.get_subject().CN == "Testing Root CA" + + def test_invalid_chain_no_root(self) -> None: + """ + `verify_certificate` raises error when a root certificate is missing + from the chain. + """ + store = X509Store() + store.add_cert(self.intermediate_cert) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + + with pytest.raises(X509StoreContextError) as exc: + store_ctx.verify_certificate() + + assert str(exc.value) == "unable to get issuer certificate" + assert exc.value.certificate.get_subject().CN == "intermediate" + + def test_invalid_chain_no_intermediate(self) -> None: + """ + `verify_certificate` raises error when an intermediate certificate is + missing from the chain. + """ + store = X509Store() + store.add_cert(self.root_cert) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + + with pytest.raises(X509StoreContextError) as exc: + store_ctx.verify_certificate() + + assert str(exc.value) == "unable to get local issuer certificate" + assert exc.value.certificate.get_subject().CN == "intermediate-service" + + def test_modification_pre_verify(self) -> None: + """ + `verify_certificate` can use a store context modified after + instantiation. + """ + store_bad = X509Store() + store_bad.add_cert(self.intermediate_cert) + store_good = X509Store() + store_good.add_cert(self.root_cert) + store_good.add_cert(self.intermediate_cert) + store_ctx = X509StoreContext(store_bad, self.intermediate_server_cert) + + with pytest.raises(X509StoreContextError) as exc: + store_ctx.verify_certificate() + + assert str(exc.value) == "unable to get issuer certificate" + assert exc.value.certificate.get_subject().CN == "intermediate" + + store_ctx.set_store(store_good) + assert store_ctx.verify_certificate() is None # type: ignore[func-returns-value] + + def test_verify_with_time(self) -> None: + """ + `verify_certificate` raises error when the verification time is + set at notAfter. + """ + store = X509Store() + store.add_cert(self.root_cert) + store.add_cert(self.intermediate_cert) + + expire_time = self.intermediate_server_cert.get_notAfter() + assert expire_time is not None + expire_datetime = datetime.strptime( + expire_time.decode("utf-8"), "%Y%m%d%H%M%SZ" + ) + store.set_time(expire_datetime) + + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + with pytest.raises(X509StoreContextError) as exc: + store_ctx.verify_certificate() + + assert str(exc.value) == "certificate has expired" + + def test_get_verified_chain(self) -> None: + """ + `get_verified_chain` returns the verified chain. + """ + store = X509Store() + store.add_cert(self.root_cert) + store.add_cert(self.intermediate_cert) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + chain = store_ctx.get_verified_chain() + assert len(chain) == 3 + intermediate_subject = self.intermediate_server_cert.get_subject() + assert chain[0].get_subject() == intermediate_subject + assert chain[1].get_subject() == self.intermediate_cert.get_subject() + assert chain[2].get_subject() == self.root_cert.get_subject() + # Test reuse + chain = store_ctx.get_verified_chain() + assert len(chain) == 3 + assert chain[0].get_subject() == intermediate_subject + assert chain[1].get_subject() == self.intermediate_cert.get_subject() + assert chain[2].get_subject() == self.root_cert.get_subject() + + def test_get_verified_chain_invalid_chain_no_root(self) -> None: + """ + `get_verified_chain` raises error when cert verification fails. + """ + store = X509Store() + store.add_cert(self.intermediate_cert) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + + with pytest.raises(X509StoreContextError) as exc: + store_ctx.get_verified_chain() + + assert str(exc.value) == "unable to get issuer certificate" + assert exc.value.certificate.get_subject().CN == "intermediate" + + @pytest.fixture + def root_ca_file(self, tmp_path: pathlib.Path) -> pathlib.Path: + return self._create_ca_file( + tmp_path, "root_ca_hash_dir", self.root_cert + ) + + @pytest.fixture + def intermediate_ca_file(self, tmp_path: pathlib.Path) -> pathlib.Path: + return self._create_ca_file( + tmp_path, "intermediate_ca_hash_dir", self.intermediate_cert + ) + + @staticmethod + def _create_ca_file( + base_path: pathlib.Path, hash_directory: str, cacert: X509 + ) -> pathlib.Path: + ca_hash = f"{cacert.subject_name_hash():08x}.0" + cafile = base_path / hash_directory / ca_hash + cafile.parent.mkdir(parents=True, exist_ok=True) + cafile.write_bytes(dump_certificate(FILETYPE_PEM, cacert)) + return cafile + + def test_verify_with_ca_file_location( + self, root_ca_file: pathlib.Path + ) -> None: + store = X509Store() + store.load_locations(str(root_ca_file)) + + store_ctx = X509StoreContext(store, self.intermediate_cert) + store_ctx.verify_certificate() + + def test_verify_with_ca_path_location( + self, root_ca_file: pathlib.Path + ) -> None: + store = X509Store() + store.load_locations(None, str(root_ca_file.parent)) + + store_ctx = X509StoreContext(store, self.intermediate_cert) + store_ctx.verify_certificate() + + def test_verify_with_cafile_and_capath( + self, + root_ca_file: pathlib.Path, + intermediate_ca_file: pathlib.Path, + ) -> None: + store = X509Store() + store.load_locations( + cafile=str(root_ca_file), capath=str(intermediate_ca_file.parent) + ) + + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + store_ctx.verify_certificate() + + def test_verify_with_multiple_ca_files( + self, root_ca_file: pathlib.Path, intermediate_ca_file: pathlib.Path + ) -> None: + store = X509Store() + store.load_locations(str(root_ca_file)) + store.load_locations(str(intermediate_ca_file)) + + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + store_ctx.verify_certificate() + + def test_verify_failure_with_empty_ca_directory( + self, tmp_path: pathlib.Path + ) -> None: + store = X509Store() + store.load_locations(None, str(tmp_path)) + + store_ctx = X509StoreContext(store, self.intermediate_cert) + with pytest.raises(X509StoreContextError) as exc: + store_ctx.verify_certificate() + + assert str(exc.value) == "unable to get local issuer certificate" + + def test_verify_with_partial_chain(self) -> None: + store = X509Store() + store.add_cert(self.intermediate_cert) + + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + with pytest.raises(X509StoreContextError): + store_ctx.verify_certificate() + + # Now set the partial verification flag for verification. + store.set_flags(X509StoreFlags.PARTIAL_CHAIN) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + assert store_ctx.verify_certificate() is None # type: ignore[func-returns-value] + + +class TestEllipticCurve: + """ + Tests for `_EllipticCurve`, `get_elliptic_curve`, and + `get_elliptic_curves`. + """ + + def test_set(self) -> None: + """ + `get_elliptic_curves` returns a `set`. + """ + assert isinstance(get_elliptic_curves(), set) + + def test_a_curve(self) -> None: + """ + `get_elliptic_curve` can be used to retrieve a particular supported + curve. + """ + curves = get_elliptic_curves() + curve = next(iter(curves)) + assert curve.name == get_elliptic_curve(curve.name).name + + def test_not_a_curve(self) -> None: + """ + `get_elliptic_curve` raises `ValueError` if called with a name which + does not identify a supported curve. + """ + with pytest.raises(ValueError): + get_elliptic_curve("this curve was just invented") + + def test_repr(self) -> None: + """ + The string representation of a curve object includes simply states the + object is a curve and what its name is. + """ + curves = get_elliptic_curves() + curve = next(iter(curves)) + assert f"" == repr(curve) + + def test_to_EC_KEY(self) -> None: + """ + The curve object can export a version of itself as an EC_KEY* via the + private `_EllipticCurve._to_EC_KEY`. + """ + curves = get_elliptic_curves() + curve = next(iter(curves)) + # It's not easy to assert anything about this object. However, see + # leakcheck/crypto.py for a test that demonstrates it at least does + # not leak memory. + curve._to_EC_KEY() + + +class TestEllipticCurveEquality: + """ + Tests `_EllipticCurve`'s implementation of ``==`` and ``!=``. + """ + + def anInstance(self) -> _EllipticCurve: + """ + Get the curve object for an arbitrary curve supported by the system. + """ + return next(iter(get_elliptic_curves())) + + def anotherInstance(self) -> _EllipticCurve: + """ + Get the curve object for an arbitrary curve supported by the system - + but not the one returned by C{anInstance}. + """ + return list(get_elliptic_curves())[1] + + def test_identicalEq(self) -> None: + """ + An object compares equal to itself using the C{==} operator. + """ + o = self.anInstance() + assert o == o + + def test_identicalNe(self) -> None: + """ + An object doesn't compare not equal to itself using the C{!=} operator. + """ + o = self.anInstance() + assert not (o != o) + + def test_sameEq(self) -> None: + """ + Two objects that are equal to each other compare equal to each other + using the C{==} operator. + """ + a = self.anInstance() + b = self.anInstance() + assert a == b + + def test_sameNe(self) -> None: + """ + Two objects that are equal to each other do not compare not equal to + each other using the C{!=} operator. + """ + a = self.anInstance() + b = self.anInstance() + assert not (a != b) + + def test_differentEq(self) -> None: + """ + Two objects that are not equal to each other do not compare equal to + each other using the C{==} operator. + """ + a = self.anInstance() + b = self.anotherInstance() + assert not (a == b) + + def test_differentNe(self) -> None: + """ + Two objects that are not equal to each other compare not equal to each + other using the C{!=} operator. + """ + a = self.anInstance() + b = self.anotherInstance() + assert a != b + + def test_anotherTypeEq(self) -> None: + """ + The object does not compare equal to an object of an unrelated type + (which does not implement the comparison) using the C{==} operator. + """ + a = self.anInstance() + b = object() + assert not (a == b) + + def test_anotherTypeNe(self) -> None: + """ + The object compares not equal to an object of an unrelated type (which + does not implement the comparison) using the C{!=} operator. + """ + a = self.anInstance() + b = object() + assert a != b + + def test_delegatedEq(self) -> None: + """ + The result of comparison using C{==} is delegated to the right-hand + operand if it is of an unrelated type. + """ + called = False + + class Delegate: + def __eq__(self, other: object) -> bool: + nonlocal called + called = True + return False + + a = self.anInstance() + b = Delegate() + assert not (a == b) + assert called + + def test_delegateNe(self) -> None: + """ + The result of comparison using C{!=} is delegated to the right-hand + operand if it is of an unrelated type. + """ + called = False + + class Delegate: + def __ne__(self, other: object) -> bool: + nonlocal called + called = True + return False + + a = self.anInstance() + b = Delegate() + assert not (a != b) # type: ignore[comparison-overlap] + assert called + + +class TestEllipticCurveHash: + """ + Tests for `_EllipticCurve`'s implementation of hashing (thus use as + an item in a `dict` or `set`). + """ + + def test_contains(self) -> None: + """ + The ``in`` operator reports that a `set` containing a curve does + contain that curve. + """ + curve = next(iter(get_elliptic_curves())) + curves = set([curve]) + assert curve in curves + + def test_does_not_contain(self) -> None: + """ + The ``in`` operator reports that a `set` not containing a curve + does not contain that curve. + """ + all_curves = list(get_elliptic_curves()) + + curve = all_curves[0] + curves = set([all_curves[1]]) + assert curve not in curves diff --git a/tests/test_debug.py b/tests/test_debug.py new file mode 100644 index 000000000..c2332c420 --- /dev/null +++ b/tests/test_debug.py @@ -0,0 +1,10 @@ +from OpenSSL import version +from OpenSSL.debug import _env_info + + +def test_debug_info() -> None: + """ + Debug info contains correct data. + """ + # Just check a sample we control. + assert version.__version__ in _env_info diff --git a/tests/test_rand.py b/tests/test_rand.py new file mode 100644 index 000000000..6f742ab55 --- /dev/null +++ b/tests/test_rand.py @@ -0,0 +1,36 @@ +# Copyright (c) Frederick Dean +# See LICENSE for details. + +""" +Unit tests for `OpenSSL.rand`. +""" + +from __future__ import annotations + +import pytest + +from OpenSSL import rand + + +class TestRand: + @pytest.mark.parametrize("args", [(b"foo", None), (None, 3)]) + def test_add_wrong_args(self, args: tuple[object, object]) -> None: + """ + `OpenSSL.rand.add` raises `TypeError` if called with arguments not of + type `str` and `int`. + """ + with pytest.raises(TypeError): + rand.add(*args) # type: ignore[arg-type] + + def test_add(self) -> None: + """ + `OpenSSL.rand.add` adds entropy to the PRNG. + """ + rand.add(b"hamburger", 3) + + def test_status(self) -> None: + """ + `OpenSSL.rand.status` returns `1` if the PRNG has sufficient entropy, + `0` otherwise. + """ + assert rand.status() == 1 diff --git a/tests/test_ssl.py b/tests/test_ssl.py new file mode 100644 index 000000000..a551fc7b7 --- /dev/null +++ b/tests/test_ssl.py @@ -0,0 +1,5276 @@ +# Copyright (C) Jean-Paul Calderone +# See LICENSE for details. + +""" +Unit tests for :mod:`OpenSSL.SSL`. +""" + +from __future__ import annotations + +import contextlib +import datetime +import gc +import os +import pathlib +import select +import sys +import time +import typing +import uuid +from errno import ( + EAFNOSUPPORT, + ECONNREFUSED, + EINPROGRESS, + EPIPE, + ESHUTDOWN, + EWOULDBLOCK, +) +from gc import collect, get_referrers +from os import makedirs +from socket import ( + AF_INET, + AF_INET6, + MSG_PEEK, + SHUT_RDWR, + SO_RCVBUF, + SO_SNDBUF, + SOL_SOCKET, + gaierror, + socket, + socketpair, +) +from sys import getfilesystemencoding, platform +from weakref import ref + +import pytest +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.x509.oid import NameOID +from pretend import raiser + +from OpenSSL import SSL +from OpenSSL._util import ffi as _ffi +from OpenSSL._util import lib as _lib +from OpenSSL.crypto import ( + FILETYPE_PEM, + TYPE_RSA, + X509, + PKey, + X509Name, + X509Store, + dump_certificate, + dump_privatekey, + get_elliptic_curves, + load_certificate, + load_privatekey, +) +from OpenSSL.SSL import ( + DTLS_METHOD, + NO_OVERLAPPING_PROTOCOLS, + OP_COOKIE_EXCHANGE, + OP_NO_COMPRESSION, + OP_NO_QUERY_MTU, + OP_NO_TICKET, + OP_SINGLE_DH_USE, + OPENSSL_VERSION_NUMBER, + RECEIVED_SHUTDOWN, + SENT_SHUTDOWN, + SESS_CACHE_BOTH, + SESS_CACHE_CLIENT, + SESS_CACHE_NO_AUTO_CLEAR, + SESS_CACHE_NO_INTERNAL, + SESS_CACHE_NO_INTERNAL_LOOKUP, + SESS_CACHE_NO_INTERNAL_STORE, + SESS_CACHE_OFF, + SESS_CACHE_SERVER, + SSL_CB_ACCEPT_EXIT, + SSL_CB_ACCEPT_LOOP, + SSL_CB_ALERT, + SSL_CB_CONNECT_EXIT, + SSL_CB_CONNECT_LOOP, + SSL_CB_EXIT, + SSL_CB_HANDSHAKE_DONE, + SSL_CB_HANDSHAKE_START, + SSL_CB_LOOP, + SSL_CB_READ, + SSL_CB_READ_ALERT, + SSL_CB_WRITE, + SSL_CB_WRITE_ALERT, + SSL_ST_ACCEPT, + SSL_ST_CONNECT, + SSL_ST_MASK, + SSLEAY_BUILT_ON, + SSLEAY_CFLAGS, + SSLEAY_DIR, + SSLEAY_PLATFORM, + SSLEAY_VERSION, + TLS1_2_VERSION, + TLS1_3_VERSION, + TLS_METHOD, + VERIFY_CLIENT_ONCE, + VERIFY_FAIL_IF_NO_PEER_CERT, + VERIFY_NONE, + VERIFY_PEER, + Connection, + Context, + Error, + OP_NO_SSLv2, + OP_NO_SSLv3, + Session, + SSLeay_version, + SSLv23_METHOD, + SysCallError, + TLSv1_1_METHOD, + TLSv1_2_METHOD, + TLSv1_METHOD, + WantReadError, + WantWriteError, + ZeroReturnError, + _make_requires, + _NoOverlappingProtocols, +) + +from . import conftest +from .test_crypto import ( + client_cert_pem, + client_key_pem, + root_cert_pem, + root_key_pem, + server_cert_pem, + server_key_pem, +) +from .util import NON_ASCII, WARNING_TYPE_EXPECTED + +# openssl dhparam 2048 -out dh-2048.pem +dhparam = """\ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA2F5e976d/GjsaCdKv5RMWL/YV7fq1UUWpPAer5fDXflLMVUuYXxE +3m3ayZob9lbpgEU0jlPAsXHfQPGxpKmvhv+xV26V/DEoukED8JeZUY/z4pigoptl ++8+TYdNNE/rFSZQFXIp+v2D91IEgmHBnZlKFSbKR+p8i0KjExXGjU6ji3S5jkOku +ogikc7df1Ui0hWNJCmTjExq07aXghk97PsdFSxjdawuG3+vos5bnNoUwPLYlFc/z +ITYG0KXySiCLi4UDlXTZTz7u/+OYczPEgqa/JPUddbM/kfvaRAnjY38cfQ7qXf8Y +i5s5yYK7a/0eWxxRr2qraYaUj8RwDpH9CwIBAg== +-----END DH PARAMETERS----- +""" + + +def socket_any_family() -> socket: + try: + return socket(AF_INET) + except OSError as e: + if e.errno == EAFNOSUPPORT: + return socket(AF_INET6) + raise + + +def loopback_address(socket: socket) -> str: + if socket.family == AF_INET: + return "127.0.0.1" + else: + assert socket.family == AF_INET6 + return "::1" + + +def verify_cb( + conn: Connection, cert: X509, errnum: int, depth: int, ok: int +) -> bool: + return bool(ok) + + +def socket_pair() -> tuple[socket, socket]: + """ + Establish and return a pair of network sockets connected to each other. + """ + # Connect a pair of sockets + port = socket_any_family() + port.bind(("", 0)) + port.listen(1) + client = socket(port.family) + client.setblocking(False) + client.connect_ex((loopback_address(port), port.getsockname()[1])) + client.setblocking(True) + server = port.accept()[0] + + port.close() + + # Let's pass some unencrypted data to make sure our socket connection is + # fine. Just one byte, so we don't have to worry about buffers getting + # filled up or fragmentation. + server.send(b"x") + assert client.recv(1024) == b"x" + client.send(b"y") + assert server.recv(1024) == b"y" + + # Most of our callers want non-blocking sockets, make it easy for them. + server.setblocking(False) + client.setblocking(False) + + return (server, client) + + +def handshake(client: Connection, server: Connection) -> None: + conns = [client, server] + while conns: + for conn in conns: + try: + conn.do_handshake() + except WantReadError: + pass + else: + conns.remove(conn) + + +def _create_certificate_chain() -> list[tuple[PKey, X509]]: + """ + Construct and return a chain of certificates. + + 1. A new self-signed certificate authority certificate (cacert) + 2. A new intermediate certificate signed by cacert (icert) + 3. A new server certificate signed by icert (scert) + """ + not_before = datetime.datetime(2000, 1, 1, 0, 0, 0) + not_after = datetime.datetime.now() + datetime.timedelta(days=365) + + # Step 1 + cakey = rsa.generate_private_key(key_size=2048, public_exponent=65537) + casubject = x509.Name( + [x509.NameAttribute(x509.NameOID.COMMON_NAME, "Authority Certificate")] + ) + cacert = ( + x509.CertificateBuilder() + .subject_name(casubject) + .issuer_name(casubject) + .public_key(cakey.public_key()) + .not_valid_before(not_before) + .not_valid_after(not_after) + .add_extension( + x509.BasicConstraints(ca=True, path_length=None), critical=False + ) + .serial_number(1) + .sign(cakey, hashes.SHA256()) + ) + + # Step 2 + ikey = rsa.generate_private_key(key_size=2048, public_exponent=65537) + icert = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + x509.NameOID.COMMON_NAME, "Intermediate Certificate" + ) + ] + ) + ) + .issuer_name(cacert.subject) + .public_key(ikey.public_key()) + .not_valid_before(not_before) + .not_valid_after(not_after) + .add_extension( + x509.BasicConstraints(ca=True, path_length=None), critical=False + ) + .serial_number(1) + .sign(cakey, hashes.SHA256()) + ) + + # Step 3 + skey = rsa.generate_private_key(key_size=2048, public_exponent=65537) + scert = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + x509.NameOID.COMMON_NAME, "Server Certificate" + ) + ] + ) + ) + .issuer_name(icert.subject) + .public_key(skey.public_key()) + .not_valid_before(not_before) + .not_valid_after(not_after) + .add_extension( + x509.BasicConstraints(ca=False, path_length=None), critical=True + ) + .serial_number(1) + .sign(ikey, hashes.SHA256()) + ) + + return [ + (PKey.from_cryptography_key(cakey), X509.from_cryptography(cacert)), + (PKey.from_cryptography_key(ikey), X509.from_cryptography(icert)), + (PKey.from_cryptography_key(skey), X509.from_cryptography(scert)), + ] + + +def loopback_client_factory( + socket: socket, version: int = SSLv23_METHOD +) -> Connection: + client = Connection(Context(version), socket) + client.set_connect_state() + return client + + +def loopback_server_factory( + socket: socket | None, version: int = SSLv23_METHOD +) -> Connection: + ctx = Context(version) + ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) + server = Connection(ctx, socket) + server.set_accept_state() + return server + + +def loopback( + server_factory: typing.Callable[[socket], Connection] | None = None, + client_factory: typing.Callable[[socket], Connection] | None = None, +) -> tuple[Connection, Connection]: + """ + Create a connected socket pair and force two connected SSL sockets + to talk to each other. + """ + if server_factory is None: + server_factory = loopback_server_factory + if client_factory is None: + client_factory = loopback_client_factory + + (server, client) = socket_pair() + tls_server = server_factory(server) + tls_client = client_factory(client) + + handshake(tls_client, tls_server) + + tls_server.setblocking(True) + tls_client.setblocking(True) + return tls_server, tls_client + + +def interact_in_memory( + client_conn: Connection, server_conn: Connection +) -> tuple[Connection, bytes] | None: + """ + Try to read application bytes from each of the two `Connection` objects. + Copy bytes back and forth between their send/receive buffers for as long + as there is anything to copy. When there is nothing more to copy, + return `None`. If one of them actually manages to deliver some application + bytes, return a two-tuple of the connection from which the bytes were read + and the bytes themselves. + """ + wrote = True + while wrote: + # Loop until neither side has anything to say + wrote = False + + # Copy stuff from each side's send buffer to the other side's + # receive buffer. + for read, write in [ + (client_conn, server_conn), + (server_conn, client_conn), + ]: + # Give the side a chance to generate some more bytes, or succeed. + try: + data = read.recv(2**16) + except WantReadError: + # It didn't succeed, so we'll hope it generated some output. + pass + else: + # It did succeed, so we'll stop now and let the caller deal + # with it. + return (read, data) + + while True: + # Keep copying as long as there's more stuff there. + try: + dirty = read.bio_read(4096) + except WantReadError: + # Okay, nothing more waiting to be sent. Stop + # processing this send buffer. + break + else: + # Keep track of the fact that someone generated some + # output. + wrote = True + write.bio_write(dirty) + + return None + + +def handshake_in_memory( + client_conn: Connection, server_conn: Connection +) -> None: + """ + Perform the TLS handshake between two `Connection` instances connected to + each other via memory BIOs. + """ + client_conn.set_connect_state() + server_conn.set_accept_state() + + for conn in [client_conn, server_conn]: + try: + conn.do_handshake() + except WantReadError: + pass + + interact_in_memory(client_conn, server_conn) + + +def get_ssl_error_reason(ssl_error: SSL.Error) -> str | None: + """ + Extracts the reason string from the first error tuple in an SSL.Error. + Returns None if the expected error structure is not found. + """ + if ( + ssl_error.args + and isinstance(ssl_error.args, tuple) + and len(ssl_error.args) > 0 + ): + error_details = ssl_error.args[0] # list of error tuples + if isinstance(error_details, list) and len(error_details) > 0: + first_error_tuple = error_details[0] + if ( + isinstance(first_error_tuple, tuple) + and len(first_error_tuple) >= 3 + ): + reason = first_error_tuple[2] + if isinstance(reason, str): + return reason + return None + + +def create_ssl_nonblocking_connection( + mode: int | None, request_send_buffer_size: int +) -> tuple[Connection, Connection, int, int]: + """ + Create a pair of sockets and set up an SSL connection between them. + mode: The mode to set if not None. + request_send_buffer_size: requested size of the send buffer + Returns the SSL Connection objects + and the actual send/receive buffer sizes. + """ + + client_socket, server_socket = socket_pair() + + # Set up client context + client_ctx = Context(SSLv23_METHOD) + + # SSL_MODE_ENABLE_PARTIAL_WRITE and + # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER modes + # are set by default when ctx is initialized. + # Clear them if requested so tests can + # be run without them if so desired. + if mode is not None: + client_ctx.clear_mode( + _lib.SSL_MODE_ENABLE_PARTIAL_WRITE + | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + ) + # Set the new mode to the requested value + client_ctx.set_mode(mode) + + # create the SSL connections + client = Connection(client_ctx, client_socket) + server = loopback_server_factory(server_socket) + + # Allow caller to request small buffer sizes so they can be easily filled. + # Note the OS may not respect the requested values. + # Make the receive buffer smaller than the send buffer. + requested_receive_buffer_size = request_send_buffer_size // 2 + client_socket.setsockopt(SOL_SOCKET, SO_SNDBUF, request_send_buffer_size) + actual_sndbuf = client_socket.getsockopt(SOL_SOCKET, SO_SNDBUF) + + server_socket.setsockopt( + SOL_SOCKET, SO_RCVBUF, requested_receive_buffer_size + ) + actual_rcvbuf = server_socket.getsockopt(SOL_SOCKET, SO_RCVBUF) + + # set the connection state + client.set_connect_state() + # loopback_server_factory already sets the accept state on the server + + handshake(client, server) + + return ( + client, + server, + actual_sndbuf, + actual_rcvbuf, + ) + + +class TestVersion: + """ + Tests for version information exposed by `OpenSSL.SSL.SSLeay_version` and + `OpenSSL.SSL.OPENSSL_VERSION_NUMBER`. + """ + + def test_OPENSSL_VERSION_NUMBER(self) -> None: + """ + `OPENSSL_VERSION_NUMBER` is an integer with status in the low byte and + the patch, fix, minor, and major versions in the nibbles above that. + """ + assert isinstance(OPENSSL_VERSION_NUMBER, int) + + def test_SSLeay_version(self) -> None: + """ + `SSLeay_version` takes a version type indicator and returns one of a + number of version strings based on that indicator. + """ + versions = {} + for t in [ + SSLEAY_VERSION, + SSLEAY_CFLAGS, + SSLEAY_BUILT_ON, + SSLEAY_PLATFORM, + SSLEAY_DIR, + ]: + version = SSLeay_version(t) + versions[version] = t + assert isinstance(version, bytes) + assert len(versions) == 5 + + +@pytest.fixture +def ca_file(tmp_path: pathlib.Path) -> bytes: + """ + Create a valid PEM file with CA certificates and return the path. + """ + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + public_key = key.public_key() + + builder = x509.CertificateBuilder() + builder = builder.subject_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "pyopenssl.org")]) + ) + builder = builder.issuer_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "pyopenssl.org")]) + ) + one_day = datetime.timedelta(1, 0, 0) + builder = builder.not_valid_before(datetime.datetime.today() - one_day) + builder = builder.not_valid_after(datetime.datetime.today() + one_day) + builder = builder.serial_number(int(uuid.uuid4())) + builder = builder.public_key(public_key) + builder = builder.add_extension( + x509.BasicConstraints(ca=True, path_length=None), + critical=True, + ) + + certificate = builder.sign(private_key=key, algorithm=hashes.SHA256()) + + ca_file = tmp_path / "test.pem" + ca_file.write_bytes( + certificate.public_bytes( + encoding=serialization.Encoding.PEM, + ) + ) + + return str(ca_file).encode("ascii") + + +@pytest.fixture +def context() -> Context: + """ + A simple "best TLS you can get" context. TLS 1.2+ in any reasonable OpenSSL + """ + return Context(SSLv23_METHOD) + + +class TestContext: + """ + Unit tests for `OpenSSL.SSL.Context`. + """ + + @pytest.mark.parametrize( + "cipher_string", + [b"hello world:AES128-SHA", "hello world:AES128-SHA"], + ) + def test_set_cipher_list( + self, context: Context, cipher_string: bytes + ) -> None: + """ + `Context.set_cipher_list` accepts both byte and unicode strings + for naming the ciphers which connections created with the context + object will be able to choose from. + """ + context.set_cipher_list(cipher_string) + conn = Connection(context, None) + + assert "AES128-SHA" in conn.get_cipher_list() + + def test_set_tls13_ciphersuites(self, context: Context) -> None: + """ + `Context.set_tls13_ciphersuites` accepts both byte and unicode strings + for naming the ciphers which connections created with the context + object will be able to choose from. + """ + context.set_tls13_ciphersuites(b"TLS_AES_128_GCM_SHA256") + conn = Connection(context, None) + + # OpenSSL has different APIs for *setting* TLS <=1.2 and >= 1.3 + # but only one API for retrieving them + assert "TLS_AES_128_GCM_SHA256" in conn.get_cipher_list() + assert "TLS_AES_256_GCM_SHA384" not in conn.get_cipher_list() + + def test_set_cipher_list_wrong_type(self, context: Context) -> None: + """ + `Context.set_cipher_list` raises `TypeError` when passed a non-string + argument. + """ + with pytest.raises(TypeError): + context.set_cipher_list(object()) # type: ignore[arg-type] + + @pytest.mark.flaky(reruns=2) + def test_set_cipher_list_no_cipher_match(self, context: Context) -> None: + """ + `Context.set_cipher_list` raises `OpenSSL.SSL.Error` with a + `"no cipher match"` reason string regardless of the TLS + version. + """ + with pytest.raises(Error) as excinfo: + context.set_cipher_list(b"imaginary-cipher") + assert excinfo.value.args[0][0] in [ + # 1.1.x + ( + "SSL routines", + "SSL_CTX_set_cipher_list", + "no cipher match", + ), + # 3.0.x + ( + "SSL routines", + "", + "no cipher match", + ), + # aws-lc + ( + "SSL routines", + "OPENSSL_internal", + "NO_CIPHER_MATCH", + ), + ] + + def test_load_client_ca(self, context: Context, ca_file: bytes) -> None: + """ + `Context.load_client_ca` works as far as we can tell. + """ + context.load_client_ca(ca_file) + + def test_load_client_ca_invalid( + self, context: Context, tmp_path: pathlib.Path + ) -> None: + """ + `Context.load_client_ca` raises an Error if the ca file is invalid. + """ + ca_file = tmp_path / "test.pem" + ca_file.write_text("") + + with pytest.raises(Error) as e: + context.load_client_ca(str(ca_file).encode("ascii")) + + assert "PEM routines" == e.value.args[0][0][0] + + def test_load_client_ca_unicode( + self, context: Context, ca_file: bytes + ) -> None: + """ + Passing the path as unicode raises a warning but works. + """ + with pytest.deprecated_call(): + context.load_client_ca(ca_file.decode("ascii")) # type: ignore[arg-type] + + def test_set_session_id(self, context: Context) -> None: + """ + `Context.set_session_id` works as far as we can tell. + """ + context.set_session_id(b"abc") + + def test_set_session_id_fail(self, context: Context) -> None: + """ + `Context.set_session_id` errors are propagated. + """ + with pytest.raises(Error) as e: + context.set_session_id(b"abc" * 1000) + + assert e.value.args[0][0] in [ + # 1.1.x + ( + "SSL routines", + "SSL_CTX_set_session_id_context", + "ssl session id context too long", + ), + # 3.0.x + ( + "SSL routines", + "", + "ssl session id context too long", + ), + # aws-lc + ( + "SSL routines", + "OPENSSL_internal", + "SSL_SESSION_ID_CONTEXT_TOO_LONG", + ), + ] + + def test_set_session_id_unicode(self, context: Context) -> None: + """ + `Context.set_session_id` raises a warning if a unicode string is + passed. + """ + with pytest.deprecated_call(): + context.set_session_id("abc") # type: ignore[arg-type] + + def test_method(self) -> None: + """ + `Context` can be instantiated with one of `SSLv2_METHOD`, + `SSLv3_METHOD`, `SSLv23_METHOD`, `TLSv1_METHOD`, `TLSv1_1_METHOD`, + or `TLSv1_2_METHOD`. + """ + methods = [SSLv23_METHOD, TLSv1_METHOD, TLSv1_1_METHOD, TLSv1_2_METHOD] + for meth in methods: + Context(meth) + + with pytest.raises(TypeError): + Context("") # type: ignore[arg-type] + with pytest.raises(ValueError): + Context(13) + + def test_use_privatekey_file_missing(self, tmpfile: bytes) -> None: + """ + `Context.use_privatekey_file` raises `OpenSSL.SSL.Error` when passed + the name of a file which does not exist. + """ + ctx = Context(SSLv23_METHOD) + with pytest.raises(Error): + ctx.use_privatekey_file(tmpfile) + + def _use_privatekey_file_test( + self, pemfile: bytes | str, filetype: int + ) -> None: + """ + Verify that calling ``Context.use_privatekey_file`` with the given + arguments does not raise an exception. + """ + key = PKey() + key.generate_key(TYPE_RSA, 1024) + + with open(pemfile, "w") as pem: + pem.write(dump_privatekey(FILETYPE_PEM, key).decode("ascii")) + + ctx = Context(SSLv23_METHOD) + ctx.use_privatekey_file(pemfile, filetype) + + @pytest.mark.parametrize("filetype", [object(), "", None, 1.0]) + def test_wrong_privatekey_file_wrong_args( + self, tmpfile: bytes, filetype: object + ) -> None: + """ + `Context.use_privatekey_file` raises `TypeError` when called with + a `filetype` which is not a valid file encoding. + """ + ctx = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + ctx.use_privatekey_file(tmpfile, filetype) # type: ignore[arg-type] + + def test_use_privatekey_file_bytes(self, tmpfile: bytes) -> None: + """ + A private key can be specified from a file by passing a ``bytes`` + instance giving the file name to ``Context.use_privatekey_file``. + """ + self._use_privatekey_file_test( + tmpfile + NON_ASCII.encode(getfilesystemencoding()), + FILETYPE_PEM, + ) + + def test_use_privatekey_file_unicode(self, tmpfile: bytes) -> None: + """ + A private key can be specified from a file by passing a ``unicode`` + instance giving the file name to ``Context.use_privatekey_file``. + """ + self._use_privatekey_file_test( + tmpfile.decode(getfilesystemencoding()) + NON_ASCII, + FILETYPE_PEM, + ) + + def test_use_certificate_file_wrong_args(self) -> None: + """ + `Context.use_certificate_file` raises `TypeError` if the first + argument is not a byte string or the second argument is not an integer. + """ + ctx = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + ctx.use_certificate_file(object(), FILETYPE_PEM) # type: ignore[arg-type] + with pytest.raises(TypeError): + ctx.use_certificate_file(b"somefile", object()) # type: ignore[arg-type] + with pytest.raises(TypeError): + ctx.use_certificate_file(object(), FILETYPE_PEM) # type: ignore[arg-type] + + def test_use_certificate_file_missing(self, tmpfile: bytes) -> None: + """ + `Context.use_certificate_file` raises `OpenSSL.SSL.Error` if passed + the name of a file which does not exist. + """ + ctx = Context(SSLv23_METHOD) + with pytest.raises(Error): + ctx.use_certificate_file(tmpfile) + + def _use_certificate_file_test( + self, certificate_file: bytes | str + ) -> None: + """ + Verify that calling ``Context.use_certificate_file`` with the given + filename doesn't raise an exception. + """ + # TODO + # Hard to assert anything. But we could set a privatekey then ask + # OpenSSL if the cert and key agree using check_privatekey. Then as + # long as check_privatekey works right we're good... + with open(certificate_file, "wb") as pem_file: + pem_file.write(root_cert_pem) + + ctx = Context(SSLv23_METHOD) + ctx.use_certificate_file(certificate_file) + + def test_use_certificate_file_bytes(self, tmpfile: bytes) -> None: + """ + `Context.use_certificate_file` sets the certificate (given as a + `bytes` filename) which will be used to identify connections created + using the context. + """ + filename = tmpfile + NON_ASCII.encode(getfilesystemencoding()) + self._use_certificate_file_test(filename) + + def test_use_certificate_file_unicode(self, tmpfile: bytes) -> None: + """ + `Context.use_certificate_file` sets the certificate (given as a + `bytes` filename) which will be used to identify connections created + using the context. + """ + filename = tmpfile.decode(getfilesystemencoding()) + NON_ASCII + self._use_certificate_file_test(filename) + + def test_check_privatekey_valid(self) -> None: + """ + `Context.check_privatekey` returns `None` if the `Context` instance + has been configured to use a matched key and certificate pair. + """ + key = load_privatekey(FILETYPE_PEM, client_key_pem) + cert = load_certificate(FILETYPE_PEM, client_cert_pem) + context = Context(SSLv23_METHOD) + context.use_privatekey(key) + context.use_certificate(cert) + assert context.check_privatekey() is None # type: ignore[func-returns-value] + + context = Context(SSLv23_METHOD) + cryptography_key = key.to_cryptography_key() + assert isinstance(cryptography_key, rsa.RSAPrivateKey) + context.use_privatekey(cryptography_key) + context.use_certificate(cert) + assert context.check_privatekey() is None # type: ignore[func-returns-value] + + def test_check_privatekey_invalid(self) -> None: + """ + `Context.check_privatekey` raises `Error` if the `Context` instance + has been configured to use a key and certificate pair which don't + relate to each other. + """ + key = load_privatekey(FILETYPE_PEM, client_key_pem) + cert = load_certificate(FILETYPE_PEM, server_cert_pem) + context = Context(SSLv23_METHOD) + context.use_privatekey(key) + context.use_certificate(cert) + with pytest.raises(Error): + context.check_privatekey() + + context = Context(SSLv23_METHOD) + cryptography_key = key.to_cryptography_key() + assert isinstance(cryptography_key, rsa.RSAPrivateKey) + context.use_privatekey(cryptography_key) + context.use_certificate(cert) + with pytest.raises(Error): + context.check_privatekey() + + def test_app_data(self) -> None: + """ + `Context.set_app_data` stores an object for later retrieval + using `Context.get_app_data`. + """ + app_data = object() + context = Context(SSLv23_METHOD) + context.set_app_data(app_data) + assert context.get_app_data() is app_data + + def test_set_options_wrong_args(self) -> None: + """ + `Context.set_options` raises `TypeError` if called with + a non-`int` argument. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.set_options(None) # type: ignore[arg-type] + + def test_set_options(self) -> None: + """ + `Context.set_options` returns the new options value. + """ + context = Context(SSLv23_METHOD) + options = context.set_options(OP_NO_SSLv2) + assert options & OP_NO_SSLv2 == OP_NO_SSLv2 + + def test_set_mode_wrong_args(self) -> None: + """ + `Context.set_mode` raises `TypeError` if called with + a non-`int` argument. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.set_mode(None) # type: ignore[arg-type] + + def test_set_mode(self) -> None: + """ + `Context.set_mode` accepts a mode bitvector and returns the + newly set mode. + """ + context = Context(SSLv23_METHOD) + mode = _lib.SSL_MODE_ENABLE_PARTIAL_WRITE + assert mode & context.set_mode(mode) + + def test_set_timeout_wrong_args(self) -> None: + """ + `Context.set_timeout` raises `TypeError` if called with + a non-`int` argument. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.set_timeout(None) # type: ignore[arg-type] + + def test_timeout(self) -> None: + """ + `Context.set_timeout` sets the session timeout for all connections + created using the context object. `Context.get_timeout` retrieves + this value. + """ + context = Context(SSLv23_METHOD) + context.set_timeout(1234) + assert context.get_timeout() == 1234 + + def test_set_verify_depth_wrong_args(self) -> None: + """ + `Context.set_verify_depth` raises `TypeError` if called with a + non-`int` argument. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.set_verify_depth(None) # type: ignore[arg-type] + + def test_verify_depth(self) -> None: + """ + `Context.set_verify_depth` sets the number of certificates in + a chain to follow before giving up. The value can be retrieved with + `Context.get_verify_depth`. + """ + context = Context(SSLv23_METHOD) + context.set_verify_depth(11) + assert context.get_verify_depth() == 11 + + def _write_encrypted_pem(self, passphrase: bytes, tmpfile: bytes) -> bytes: + """ + Write a new private key out to a new file, encrypted using the given + passphrase. Return the path to the new file. + """ + key = PKey() + key.generate_key(TYPE_RSA, 1024) + pem = dump_privatekey(FILETYPE_PEM, key, "aes-256-cbc", passphrase) + with open(tmpfile, "w") as fObj: + fObj.write(pem.decode("ascii")) + return tmpfile + + def test_set_passwd_cb_wrong_args(self) -> None: + """ + `Context.set_passwd_cb` raises `TypeError` if called with a + non-callable first argument. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.set_passwd_cb(None) # type: ignore[arg-type] + + def test_set_passwd_cb(self, tmpfile: bytes) -> None: + """ + `Context.set_passwd_cb` accepts a callable which will be invoked when + a private key is loaded from an encrypted PEM. + """ + passphrase = b"foobar" + pemFile = self._write_encrypted_pem(passphrase, tmpfile) + calledWith = [] + + def passphraseCallback( + maxlen: int, verify: bool, extra: None + ) -> bytes: + calledWith.append((maxlen, verify, extra)) + return passphrase + + context = Context(SSLv23_METHOD) + context.set_passwd_cb(passphraseCallback) + context.use_privatekey_file(pemFile) + assert len(calledWith) == 1 + assert isinstance(calledWith[0][0], int) + assert isinstance(calledWith[0][1], int) + assert calledWith[0][2] is None + + def test_passwd_callback_exception(self, tmpfile: bytes) -> None: + """ + `Context.use_privatekey_file` propagates any exception raised + by the passphrase callback. + """ + pemFile = self._write_encrypted_pem(b"monkeys are nice", tmpfile) + + def passphraseCallback( + maxlen: int, verify: bool, extra: None + ) -> bytes: + raise RuntimeError("Sorry, I am a fail.") + + context = Context(SSLv23_METHOD) + context.set_passwd_cb(passphraseCallback) + with pytest.raises(RuntimeError): + context.use_privatekey_file(pemFile) + + def test_passwd_callback_false(self, tmpfile: bytes) -> None: + """ + `Context.use_privatekey_file` raises `OpenSSL.SSL.Error` if the + passphrase callback returns a false value. + """ + pemFile = self._write_encrypted_pem(b"monkeys are nice", tmpfile) + + def passphraseCallback( + maxlen: int, verify: bool, extra: None + ) -> bytes: + return b"" + + context = Context(SSLv23_METHOD) + context.set_passwd_cb(passphraseCallback) + with pytest.raises(Error): + context.use_privatekey_file(pemFile) + + def test_passwd_callback_non_string(self, tmpfile: bytes) -> None: + """ + `Context.use_privatekey_file` raises `OpenSSL.SSL.Error` if the + passphrase callback returns a true non-string value. + """ + pemFile = self._write_encrypted_pem(b"monkeys are nice", tmpfile) + + def passphraseCallback(maxlen: int, verify: bool, extra: None) -> int: + return 10 + + context = Context(SSLv23_METHOD) + context.set_passwd_cb(passphraseCallback) # type: ignore[arg-type] + # TODO: Surely this is the wrong error? + with pytest.raises(ValueError): + context.use_privatekey_file(pemFile) + + def test_passwd_callback_too_long(self, tmpfile: bytes) -> None: + """ + If the passphrase returned by the passphrase callback returns a string + longer than the indicated maximum length, it is truncated. + """ + # A priori knowledge! + passphrase = b"x" * 1024 + pemFile = self._write_encrypted_pem(passphrase, tmpfile) + + def passphraseCallback( + maxlen: int, verify: bool, extra: None + ) -> bytes: + assert maxlen == 1024 + return passphrase + b"y" + + context = Context(SSLv23_METHOD) + context.set_passwd_cb(passphraseCallback) + # This shall succeed because the truncated result is the correct + # passphrase. + context.use_privatekey_file(pemFile) + + def test_set_info_callback(self) -> None: + """ + `Context.set_info_callback` accepts a callable which will be + invoked when certain information about an SSL connection is available. + """ + (server, client) = socket_pair() + + clientSSL = Connection(Context(SSLv23_METHOD), client) + clientSSL.set_connect_state() + + called = [] + + def info(conn: Connection, where: int, ret: int) -> None: + called.append((conn, where, ret)) + + context = Context(SSLv23_METHOD) + context.set_info_callback(info) + context.use_certificate(load_certificate(FILETYPE_PEM, root_cert_pem)) + context.use_privatekey(load_privatekey(FILETYPE_PEM, root_key_pem)) + + serverSSL = Connection(context, server) + serverSSL.set_accept_state() + + handshake(clientSSL, serverSSL) + + # The callback must always be called with a Connection instance as the + # first argument. It would probably be better to split this into + # separate tests for client and server side info callbacks so we could + # assert it is called with the right Connection instance. It would + # also be good to assert *something* about `where` and `ret`. + notConnections = [ + conn + for (conn, where, ret) in called + if not isinstance(conn, Connection) + ] + assert [] == notConnections, ( + "Some info callback arguments were not Connection instances." + ) + + @pytest.mark.skipif( + not getattr(_lib, "Cryptography_HAS_KEYLOG", None), + reason="SSL_CTX_set_keylog_callback unavailable", + ) + def test_set_keylog_callback(self) -> None: + """ + `Context.set_keylog_callback` accepts a callable which will be + invoked when key material is generated or received. + """ + called = [] + + def keylog(conn: Connection, line: bytes) -> None: + called.append((conn, line)) + + server_context = Context(TLSv1_2_METHOD) + server_context.set_keylog_callback(keylog) + server_context.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + server_context.use_privatekey( + load_privatekey(FILETYPE_PEM, root_key_pem) + ) + + client_context = Context(SSLv23_METHOD) + + self._handshake_test(server_context, client_context) + + assert called + assert all(isinstance(conn, Connection) for conn, line in called) + assert all(b"CLIENT_RANDOM" in line for conn, line in called) + + def test_set_proto_version(self) -> None: + high_version = TLS1_3_VERSION + low_version = TLS1_2_VERSION + + server_context = Context(TLS_METHOD) + server_context.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + server_context.use_privatekey( + load_privatekey(FILETYPE_PEM, root_key_pem) + ) + server_context.set_min_proto_version(high_version) + + client_context = Context(TLS_METHOD) + client_context.set_max_proto_version(low_version) + + with pytest.raises(Error, match=r"(?i)unsupported.protocol"): + self._handshake_test(server_context, client_context) + + client_context = Context(TLS_METHOD) + client_context.set_max_proto_version(0) + self._handshake_test(server_context, client_context) + + def _load_verify_locations_test( + self, cafile: bytes | str | None, capath: bytes | str | None = None + ) -> None: + """ + Create a client context which will verify the peer certificate and call + its `load_verify_locations` method with the given arguments. + Then connect it to a server and ensure that the handshake succeeds. + """ + (server, client) = socket_pair() + + clientContext = Context(SSLv23_METHOD) + clientContext.load_verify_locations(cafile, capath) + # Require that the server certificate verify properly or the + # connection will fail. + clientContext.set_verify( + VERIFY_PEER, + lambda conn, cert, errno, depth, preverify_ok: bool(preverify_ok), + ) + + clientSSL = Connection(clientContext, client) + clientSSL.set_connect_state() + + serverContext = Context(SSLv23_METHOD) + serverContext.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + serverContext.use_privatekey( + load_privatekey(FILETYPE_PEM, root_key_pem) + ) + + serverSSL = Connection(serverContext, server) + serverSSL.set_accept_state() + + # Without load_verify_locations above, the handshake + # will fail: + # Error: [('SSL routines', 'SSL3_GET_SERVER_CERTIFICATE', + # 'certificate verify failed')] + handshake(clientSSL, serverSSL) + + cert = clientSSL.get_peer_certificate() + assert cert is not None + assert cert.get_subject().CN == "Testing Root CA" + + cryptography_cert = clientSSL.get_peer_certificate( + as_cryptography=True + ) + assert cryptography_cert is not None + assert ( + cryptography_cert.subject.rfc4514_string() + == "CN=Testing Root CA,O=Testing,L=Chicago,ST=IL,C=US" + ) + + def _load_verify_cafile(self, cafile: str | bytes) -> None: + """ + Verify that if path to a file containing a certificate is passed to + `Context.load_verify_locations` for the ``cafile`` parameter, that + certificate is used as a trust root for the purposes of verifying + connections created using that `Context`. + """ + with open(cafile, "w") as fObj: + fObj.write(root_cert_pem.decode("ascii")) + + self._load_verify_locations_test(cafile) + + def test_load_verify_bytes_cafile(self, tmpfile: bytes) -> None: + """ + `Context.load_verify_locations` accepts a file name as a `bytes` + instance and uses the certificates within for verification purposes. + """ + cafile = tmpfile + NON_ASCII.encode(getfilesystemencoding()) + self._load_verify_cafile(cafile) + + def test_load_verify_unicode_cafile(self, tmpfile: bytes) -> None: + """ + `Context.load_verify_locations` accepts a file name as a `unicode` + instance and uses the certificates within for verification purposes. + """ + self._load_verify_cafile( + tmpfile.decode(getfilesystemencoding()) + NON_ASCII + ) + + def test_load_verify_invalid_file(self, tmpfile: bytes) -> None: + """ + `Context.load_verify_locations` raises `Error` when passed a + non-existent cafile. + """ + clientContext = Context(SSLv23_METHOD) + with pytest.raises(Error): + clientContext.load_verify_locations(tmpfile) + + def _load_verify_directory_locations_capath( + self, capath: str | bytes + ) -> None: + """ + Verify that if path to a directory containing certificate files is + passed to ``Context.load_verify_locations`` for the ``capath`` + parameter, those certificates are used as trust roots for the purposes + of verifying connections created using that ``Context``. + """ + makedirs(capath) + # Hash values computed manually with c_rehash to avoid depending on + # c_rehash in the test suite. One is from OpenSSL 0.9.8, the other + # from OpenSSL 1.0.0. + for name in [b"c7adac82.0", b"c3705638.0"]: + cafile: str | bytes + if isinstance(capath, str): + cafile = os.path.join(capath, name.decode()) + else: + cafile = os.path.join(capath, name) + with open(cafile, "w") as fObj: + fObj.write(root_cert_pem.decode("ascii")) + + self._load_verify_locations_test(None, capath) + + @pytest.mark.parametrize( + "pathtype", + [ + "ascii_path", + pytest.param( + "unicode_path", + marks=pytest.mark.skipif( + platform == "win32", + reason="Unicode paths not supported on Windows", + ), + ), + ], + ) + @pytest.mark.parametrize("argtype", ["bytes_arg", "unicode_arg"]) + def test_load_verify_directory_capath( + self, pathtype: str, argtype: str, tmpfile: bytes + ) -> None: + """ + `Context.load_verify_locations` accepts a directory name as a `bytes` + instance and uses the certificates within for verification purposes. + """ + if pathtype == "unicode_path": + tmpfile += NON_ASCII.encode(getfilesystemencoding()) + + if argtype == "unicode_arg": + self._load_verify_directory_locations_capath( + tmpfile.decode(getfilesystemencoding()) + ) + else: + self._load_verify_directory_locations_capath(tmpfile) + + def test_load_verify_locations_wrong_args(self) -> None: + """ + `Context.load_verify_locations` raises `TypeError` if with non-`str` + arguments. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.load_verify_locations(object()) # type: ignore[arg-type] + with pytest.raises(TypeError): + context.load_verify_locations(object(), object()) # type: ignore[arg-type] + + @pytest.mark.skipif( + not platform.startswith("linux"), + reason="Loading fallback paths is a linux-specific behavior to " + "accommodate pyca/cryptography manylinux wheels", + ) + def test_fallback_default_verify_paths( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """ + Test that we load certificates successfully on linux from the fallback + path. To do this we set the _CRYPTOGRAPHY_MANYLINUX_CA_FILE and + _CRYPTOGRAPHY_MANYLINUX_CA_DIR vars to be equal to whatever the + current OpenSSL default is and we disable + SSL_CTX_SET_default_verify_paths so that it can't find certs unless + it loads via fallback. + """ + context = Context(SSLv23_METHOD) + monkeypatch.setattr( + _lib, "SSL_CTX_set_default_verify_paths", lambda x: 1 + ) + monkeypatch.setattr( + SSL, + "_CRYPTOGRAPHY_MANYLINUX_CA_FILE", + _ffi.string(_lib.X509_get_default_cert_file()), + ) + monkeypatch.setattr( + SSL, + "_CRYPTOGRAPHY_MANYLINUX_CA_DIR", + _ffi.string(_lib.X509_get_default_cert_dir()), + ) + context.set_default_verify_paths() + store = context.get_cert_store() + assert store is not None + sk_obj = _lib.X509_STORE_get0_objects(store._store) + assert sk_obj != _ffi.NULL + num = _lib.sk_X509_OBJECT_num(sk_obj) + assert num != 0 + + def test_check_env_vars(self, monkeypatch: pytest.MonkeyPatch) -> None: + """ + Test that we return True/False appropriately if the env vars are set. + """ + context = Context(SSLv23_METHOD) + dir_var = "CUSTOM_DIR_VAR" + file_var = "CUSTOM_FILE_VAR" + assert context._check_env_vars_set(dir_var, file_var) is False + monkeypatch.setenv(dir_var, "value") + monkeypatch.setenv(file_var, "value") + assert context._check_env_vars_set(dir_var, file_var) is True + assert context._check_env_vars_set(dir_var, file_var) is True + + def test_verify_no_fallback_if_env_vars_set( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """ + Test that we don't use the fallback path if env vars are set. + """ + context = Context(SSLv23_METHOD) + monkeypatch.setattr( + _lib, "SSL_CTX_set_default_verify_paths", lambda x: 1 + ) + monkeypatch.setenv("SSL_CERT_DIR", "value") + monkeypatch.setenv("SSL_CERT_FILE", "value") + context.set_default_verify_paths() + + monkeypatch.setattr( + context, "_fallback_default_verify_paths", raiser(SystemError) + ) + context.set_default_verify_paths() + + @pytest.mark.skipif( + platform == "win32", + reason="set_default_verify_paths appears not to work on Windows. " + "See LP#404343 and LP#404344.", + ) + def test_set_default_verify_paths(self) -> None: + """ + `Context.set_default_verify_paths` causes the platform-specific CA + certificate locations to be used for verification purposes. + """ + # Testing this requires a server with a certificate signed by one + # of the CAs in the platform CA location. Getting one of those + # costs money. Fortunately (or unfortunately, depending on your + # perspective), it's easy to think of a public server on the + # internet which has such a certificate. Connecting to the network + # in a unit test is bad, but it's the only way I can think of to + # really test this. -exarkun + context = Context(SSLv23_METHOD) + context.set_default_verify_paths() + context.set_verify( + VERIFY_PEER, + lambda conn, cert, errno, depth, preverify_ok: bool(preverify_ok), + ) + + client = socket_any_family() + try: + client.connect(("encrypted.google.com", 443)) + except gaierror: + pytest.skip("cannot connect to encrypted.google.com") + clientSSL = Connection(context, client) + clientSSL.set_connect_state() + clientSSL.set_tlsext_host_name(b"encrypted.google.com") + clientSSL.do_handshake() + clientSSL.send(b"GET / HTTP/1.0\r\n\r\n") + assert clientSSL.recv(1024) + + def test_fallback_path_is_not_file_or_dir(self) -> None: + """ + Test that when passed empty arrays or paths that do not exist no + errors are raised. + """ + context = Context(SSLv23_METHOD) + context._fallback_default_verify_paths([], []) + context._fallback_default_verify_paths(["/not/a/file"], ["/not/a/dir"]) + + def test_add_extra_chain_cert_invalid_cert(self) -> None: + """ + `Context.add_extra_chain_cert` raises `TypeError` if called with an + object which is not an instance of `X509`. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.add_extra_chain_cert(object()) # type: ignore[arg-type] + + def _handshake_test( + self, serverContext: Context, clientContext: Context + ) -> None: + """ + Verify that a client and server created with the given contexts can + successfully handshake and communicate. + """ + serverSocket, clientSocket = socket_pair() + + with serverSocket, clientSocket: + server = Connection(serverContext, serverSocket) + server.set_accept_state() + + client = Connection(clientContext, clientSocket) + client.set_connect_state() + + # Make them talk to each other. + for _ in range(3): + for s in [client, server]: + try: + s.do_handshake() + except WantReadError: + select.select([client, server], [], []) + + def test_set_verify_callback_connection_argument(self) -> None: + """ + The first argument passed to the verify callback is the + `Connection` instance for which verification is taking place. + """ + serverContext = Context(SSLv23_METHOD) + serverContext.use_privatekey( + load_privatekey(FILETYPE_PEM, root_key_pem) + ) + serverContext.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + serverConnection = Connection(serverContext, None) + + class VerifyCallback: + def callback( + self, + connection: Connection, + cert: X509, + err: int, + depth: int, + ok: int, + ) -> bool: + self.connection = connection + return True + + verify = VerifyCallback() + clientContext = Context(SSLv23_METHOD) + clientContext.set_verify(VERIFY_PEER, verify.callback) + clientConnection = Connection(clientContext, None) + clientConnection.set_connect_state() + + handshake_in_memory(clientConnection, serverConnection) + + assert verify.connection is clientConnection + + def test_x509_in_verify_works(self) -> None: + """ + We had a bug where the X509 cert instantiated in the callback wrapper + didn't __init__ so it was missing objects needed when calling + get_subject. This test sets up a handshake where we call get_subject + on the cert provided to the verify callback. + """ + serverContext = Context(SSLv23_METHOD) + serverContext.use_privatekey( + load_privatekey(FILETYPE_PEM, root_key_pem) + ) + serverContext.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + serverConnection = Connection(serverContext, None) + + def verify_cb_get_subject( + conn: Connection, cert: X509, errnum: int, depth: int, ok: int + ) -> bool: + assert cert.get_subject() + return True + + clientContext = Context(SSLv23_METHOD) + clientContext.set_verify(VERIFY_PEER, verify_cb_get_subject) + clientConnection = Connection(clientContext, None) + clientConnection.set_connect_state() + + handshake_in_memory(clientConnection, serverConnection) + + def test_set_verify_callback_exception(self) -> None: + """ + If the verify callback passed to `Context.set_verify` raises an + exception, verification fails and the exception is propagated to the + caller of `Connection.do_handshake`. + """ + serverContext = Context(TLSv1_2_METHOD) + serverContext.use_privatekey( + load_privatekey(FILETYPE_PEM, root_key_pem) + ) + serverContext.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + + clientContext = Context(TLSv1_2_METHOD) + + def verify_callback( + conn: Connection, cert: X509, err: int, depth: int, ok: int + ) -> bool: + raise Exception("silly verify failure") + + clientContext.set_verify(VERIFY_PEER, verify_callback) + + with pytest.raises(Exception) as exc: + self._handshake_test(serverContext, clientContext) + + assert "silly verify failure" == str(exc.value) + + def test_set_verify_callback_reference(self) -> None: + """ + If the verify callback passed to `Context.set_verify` is set multiple + times, the pointers to the old call functions should not be dangling + and trigger a segfault. + """ + serverContext = Context(TLSv1_2_METHOD) + serverContext.use_privatekey( + load_privatekey(FILETYPE_PEM, root_key_pem) + ) + serverContext.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + + clients = [] + + for i in range(5): + + def verify_callback(*args: object) -> bool: + return True + + # Create a fresh client context for each iteration since contexts + # cannot be mutated after use + clientContext = Context(TLSv1_2_METHOD) + clientContext.set_verify(VERIFY_PEER, verify_callback) + + serverSocket, clientSocket = socket_pair() + client = Connection(clientContext, clientSocket) + + clients.append((serverSocket, client)) + + gc.collect() + + # Make them talk to each other. + for serverSocket, client in clients: + server = Connection(serverContext, serverSocket) + server.set_accept_state() + client.set_connect_state() + + for _ in range(5): + for s in [client, server]: + try: + s.do_handshake() + except WantReadError: + pass + + @pytest.mark.parametrize("mode", [SSL.VERIFY_PEER, SSL.VERIFY_NONE]) + def test_set_verify_default_callback(self, mode: int) -> None: + """ + If the verify callback is omitted, the preverify value is used. + """ + serverContext = Context(TLSv1_2_METHOD) + serverContext.use_privatekey( + load_privatekey(FILETYPE_PEM, root_key_pem) + ) + serverContext.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + + clientContext = Context(TLSv1_2_METHOD) + clientContext.set_verify(mode, None) + + if mode == SSL.VERIFY_PEER: + with pytest.raises(Exception) as exc: + self._handshake_test(serverContext, clientContext) + assert "certificate verify failed" in str( + exc.value + ) or "CERTIFICATE_VERIFY_FAILED" in str(exc.value) + else: + self._handshake_test(serverContext, clientContext) + + def test_add_extra_chain_cert(self, tmp_path: pathlib.Path) -> None: + """ + `Context.add_extra_chain_cert` accepts an `X509` + instance to add to the certificate chain. + + See `_create_certificate_chain` for the details of the + certificate chain tested. + + The chain is tested by starting a server with scert and connecting + to it with a client which trusts cacert and requires verification to + succeed. + """ + chain = _create_certificate_chain() + [(cakey, cacert), (ikey, icert), (skey, scert)] = chain + + # Dump the CA certificate to a file because that's the only way to load + # it as a trusted CA in the client context. + for cert, name in [ + (cacert, "ca.pem"), + (icert, "i.pem"), + (scert, "s.pem"), + ]: + with (tmp_path / name).open("w") as f: + f.write(dump_certificate(FILETYPE_PEM, cert).decode("ascii")) + + for key, name in [(cakey, "ca.key"), (ikey, "i.key"), (skey, "s.key")]: + with (tmp_path / name).open("w") as f: + f.write(dump_privatekey(FILETYPE_PEM, key).decode("ascii")) + + # Create the server context + serverContext = Context(SSLv23_METHOD) + serverContext.use_privatekey(skey) + serverContext.use_certificate(scert) + # The client already has cacert, we only need to give them icert. + serverContext.add_extra_chain_cert(icert) + + # Create the client + clientContext = Context(SSLv23_METHOD) + clientContext.set_verify( + VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb + ) + clientContext.load_verify_locations(str(tmp_path / "ca.pem")) + + # Try it out. + self._handshake_test(serverContext, clientContext) + + def _use_certificate_chain_file_test(self, certdir: str | bytes) -> None: + """ + Verify that `Context.use_certificate_chain_file` reads a + certificate chain from a specified file. + + The chain is tested by starting a server with scert and connecting to + it with a client which trusts cacert and requires verification to + succeed. + """ + [(_, cacert), (_, icert), (skey, scert)] = _create_certificate_chain() + + makedirs(certdir) + + chainFile: str | bytes + caFile: str | bytes + if isinstance(certdir, str): + chainFile = os.path.join(certdir, "chain.pem") + caFile = os.path.join(certdir, "ca.pem") + else: + chainFile = os.path.join(certdir, b"chain.pem") + caFile = os.path.join(certdir, b"ca.pem") + + # Write out the chain file. + with open(chainFile, "wb") as fObj: + # Most specific to least general. + fObj.write(dump_certificate(FILETYPE_PEM, scert)) + fObj.write(dump_certificate(FILETYPE_PEM, icert)) + fObj.write(dump_certificate(FILETYPE_PEM, cacert)) + + with open(caFile, "w") as fObj: + fObj.write(dump_certificate(FILETYPE_PEM, cacert).decode("ascii")) + + serverContext = Context(SSLv23_METHOD) + serverContext.use_certificate_chain_file(chainFile) + serverContext.use_privatekey(skey) + + clientContext = Context(SSLv23_METHOD) + clientContext.set_verify( + VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb + ) + clientContext.load_verify_locations(caFile) + + self._handshake_test(serverContext, clientContext) + + def test_use_certificate_chain_file_bytes(self, tmpfile: bytes) -> None: + """ + ``Context.use_certificate_chain_file`` accepts the name of a file (as + an instance of ``bytes``) to specify additional certificates to use to + construct and verify a trust chain. + """ + self._use_certificate_chain_file_test( + tmpfile + NON_ASCII.encode(getfilesystemencoding()) + ) + + def test_use_certificate_chain_file_unicode(self, tmpfile: bytes) -> None: + """ + ``Context.use_certificate_chain_file`` accepts the name of a file (as + an instance of ``unicode``) to specify additional certificates to use + to construct and verify a trust chain. + """ + self._use_certificate_chain_file_test( + tmpfile.decode(getfilesystemencoding()) + NON_ASCII + ) + + def test_use_certificate_chain_file_wrong_args(self) -> None: + """ + `Context.use_certificate_chain_file` raises `TypeError` if passed a + non-byte string single argument. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.use_certificate_chain_file(object()) # type: ignore[arg-type] + + def test_use_certificate_chain_file_missing_file( + self, tmpfile: bytes + ) -> None: + """ + `Context.use_certificate_chain_file` raises `OpenSSL.SSL.Error` when + passed a bad chain file name (for example, the name of a file which + does not exist). + """ + context = Context(SSLv23_METHOD) + with pytest.raises(Error): + context.use_certificate_chain_file(tmpfile) + + def test_set_verify_mode(self) -> None: + """ + `Context.get_verify_mode` returns the verify mode flags previously + passed to `Context.set_verify`. + """ + context = Context(SSLv23_METHOD) + assert context.get_verify_mode() == 0 + context.set_verify(VERIFY_PEER | VERIFY_CLIENT_ONCE) + assert context.get_verify_mode() == (VERIFY_PEER | VERIFY_CLIENT_ONCE) + + @pytest.mark.parametrize("mode", [None, 1.0, object(), "mode"]) + def test_set_verify_wrong_mode_arg(self, mode: object) -> None: + """ + `Context.set_verify` raises `TypeError` if the first argument is + not an integer. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.set_verify(mode=mode) # type: ignore[arg-type] + + @pytest.mark.parametrize("callback", [1.0, "mode", ("foo", "bar")]) + def test_set_verify_wrong_callable_arg(self, callback: object) -> None: + """ + `Context.set_verify` raises `TypeError` if the second argument + is not callable. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.set_verify(mode=VERIFY_PEER, callback=callback) # type: ignore[arg-type] + + def test_load_tmp_dh_wrong_args(self) -> None: + """ + `Context.load_tmp_dh` raises `TypeError` if called with a + non-`str` argument. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.load_tmp_dh(object()) # type: ignore[arg-type] + + def test_load_tmp_dh_missing_file(self) -> None: + """ + `Context.load_tmp_dh` raises `OpenSSL.SSL.Error` if the + specified file does not exist. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(Error): + context.load_tmp_dh(b"hello") + + def _load_tmp_dh_test(self, dhfilename: bytes | str) -> None: + """ + Verify that calling ``Context.load_tmp_dh`` with the given filename + does not raise an exception. + """ + context = Context(SSLv23_METHOD) + with open(dhfilename, "w") as dhfile: + dhfile.write(dhparam) + + context.load_tmp_dh(dhfilename) + + def test_load_tmp_dh_bytes(self, tmpfile: bytes) -> None: + """ + `Context.load_tmp_dh` loads Diffie-Hellman parameters from the + specified file (given as ``bytes``). + """ + self._load_tmp_dh_test( + tmpfile + NON_ASCII.encode(getfilesystemencoding()), + ) + + def test_load_tmp_dh_unicode(self, tmpfile: bytes) -> None: + """ + `Context.load_tmp_dh` loads Diffie-Hellman parameters from the + specified file (given as ``unicode``). + """ + self._load_tmp_dh_test( + tmpfile.decode(getfilesystemencoding()) + NON_ASCII, + ) + + def test_set_tmp_ecdh(self) -> None: + """ + `Context.set_tmp_ecdh` sets the elliptic curve for Diffie-Hellman to + the specified curve. + """ + context = Context(SSLv23_METHOD) + for curve in get_elliptic_curves(): + if curve.name.startswith("Oakley-"): + # Setting Oakley-EC2N-4 and Oakley-EC2N-3 adds + # ('bignum routines', 'BN_mod_inverse', 'no inverse') to the + # error queue on OpenSSL 1.0.2. + continue + # The only easily "assertable" thing is that it does not raise an + # exception. + with pytest.deprecated_call(): + context.set_tmp_ecdh(curve) + + awslc_unsupported_curves = { + "BRAINPOOLP256R1", + "BRAINPOOLP384R1", + "BRAINPOOLP512R1", + "SECP192R1", + "SECT163K1", + "SECT163R2", + "SECT233K1", + "SECT233R1", + "SECT283K1", + "SECT283R1", + "SECT409K1", + "SECT409R1", + "SECT571K1", + "SECT571R1", + } + for name in dir(ec.EllipticCurveOID): + if name.startswith("_"): + continue + if conftest.is_awslc and name in awslc_unsupported_curves: + continue + oid = getattr(ec.EllipticCurveOID, name) + cryptography_curve = ec.get_curve_for_oid(oid) + context.set_tmp_ecdh(cryptography_curve()) + + def test_set_session_cache_mode_wrong_args(self) -> None: + """ + `Context.set_session_cache_mode` raises `TypeError` if called with + a non-integer argument. + called with other than one integer argument. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.set_session_cache_mode(object()) # type: ignore[arg-type] + + def test_session_cache_mode(self) -> None: + """ + `Context.set_session_cache_mode` specifies how sessions are cached. + The setting can be retrieved via `Context.get_session_cache_mode`. + """ + context = Context(SSLv23_METHOD) + context.set_session_cache_mode(SESS_CACHE_OFF) + off = context.set_session_cache_mode(SESS_CACHE_BOTH) + assert SESS_CACHE_OFF == off + assert SESS_CACHE_BOTH == context.get_session_cache_mode() + + def test_get_cert_store(self) -> None: + """ + `Context.get_cert_store` returns a `X509Store` instance. + """ + context = Context(SSLv23_METHOD) + store = context.get_cert_store() + assert isinstance(store, X509Store) + + def test_set_tlsext_use_srtp_not_bytes(self) -> None: + """ + `Context.set_tlsext_use_srtp' enables negotiating SRTP keying material. + + It raises a TypeError if the list of profiles is not a byte string. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + context.set_tlsext_use_srtp("SRTP_AES128_CM_SHA1_80") # type: ignore[arg-type] + + def test_set_tlsext_use_srtp_invalid_profile(self) -> None: + """ + `Context.set_tlsext_use_srtp' enables negotiating SRTP keying material. + + It raises an Error if the call to OpenSSL fails. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(Error): + context.set_tlsext_use_srtp(b"SRTP_BOGUS") + + def test_set_tlsext_use_srtp_valid(self) -> None: + """ + `Context.set_tlsext_use_srtp' enables negotiating SRTP keying material. + + It does not return anything. + """ + context = Context(SSLv23_METHOD) + assert context.set_tlsext_use_srtp(b"SRTP_AES128_CM_SHA1_80") is None + + +class TestServerNameCallback: + """ + Tests for `Context.set_tlsext_servername_callback` and its + interaction with `Connection`. + """ + + def test_old_callback_forgotten(self) -> None: + """ + If `Context.set_tlsext_servername_callback` is used to specify + a new callback, the one it replaces is dereferenced. + """ + + def callback(connection: Connection) -> None: # pragma: no cover + pass + + def replacement(connection: Connection) -> None: # pragma: no cover + pass + + context = Context(SSLv23_METHOD) + context.set_tlsext_servername_callback(callback) + + tracker = ref(callback) + del callback + + context.set_tlsext_servername_callback(replacement) + + # One run of the garbage collector happens to work on CPython. PyPy + # doesn't collect the underlying object until a second run for whatever + # reason. That's fine, it still demonstrates our code has properly + # dropped the reference. + collect() + collect() + + callback_ref = tracker() + if callback_ref is not None: + referrers = get_referrers(callback_ref) + assert len(referrers) == 1 + + def test_no_servername(self) -> None: + """ + When a client specifies no server name, the callback passed to + `Context.set_tlsext_servername_callback` is invoked and the + result of `Connection.get_servername` is `None`. + """ + args = [] + + def servername(conn: Connection) -> None: + args.append((conn, conn.get_servername())) + + context = Context(SSLv23_METHOD) + context.set_tlsext_servername_callback(servername) + + # Lose our reference to it. The Context is responsible for keeping it + # alive now. + del servername + collect() + + # Necessary to actually accept the connection + context.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(context, None) + server.set_accept_state() + + client = Connection(Context(SSLv23_METHOD), None) + client.set_connect_state() + + interact_in_memory(server, client) + + assert args == [(server, None)] + + def test_servername(self) -> None: + """ + When a client specifies a server name in its hello message, the + callback passed to `Contexts.set_tlsext_servername_callback` is + invoked and the result of `Connection.get_servername` is that + server name. + """ + args = [] + + def servername(conn: Connection) -> None: + args.append((conn, conn.get_servername())) + + context = Context(SSLv23_METHOD) + context.set_tlsext_servername_callback(servername) + + # Necessary to actually accept the connection + context.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(context, None) + server.set_accept_state() + + client = Connection(Context(SSLv23_METHOD), None) + client.set_connect_state() + client.set_tlsext_host_name(b"foo1.example.com") + + interact_in_memory(server, client) + + assert args == [(server, b"foo1.example.com")] + + def test_servername_callback_exception( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """ + When the callback passed to `Context.set_tlsext_servername_callback` + raises an exception, ``sys.excepthook`` is called with the exception + and the handshake fails with an ``Error``. + """ + exc = TypeError("server name callback failed") + + def servername(conn: Connection) -> None: + raise exc + + excepthook_calls: list[ + tuple[type[BaseException], BaseException, object] + ] = [] + + def custom_excepthook( + exc_type: type[BaseException], + exc_value: BaseException, + exc_tb: object, + ) -> None: + excepthook_calls.append((exc_type, exc_value, exc_tb)) + + context = Context(SSLv23_METHOD) + context.set_tlsext_servername_callback(servername) + + # Necessary to actually accept the connection + context.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(context, None) + server.set_accept_state() + + client = Connection(Context(SSLv23_METHOD), None) + client.set_connect_state() + client.set_tlsext_host_name(b"foo1.example.com") + + monkeypatch.setattr(sys, "excepthook", custom_excepthook) + with pytest.raises(Error): + interact_in_memory(server, client) + + assert len(excepthook_calls) == 1 + assert excepthook_calls[0][0] is TypeError + assert excepthook_calls[0][1] is exc + assert excepthook_calls[0][2] is not None + + +class TestApplicationLayerProtoNegotiation: + """ + Tests for ALPN in PyOpenSSL. + """ + + def test_alpn_success(self) -> None: + """ + Clients and servers that agree on the negotiated ALPN protocol can + correct establish a connection, and the agreed protocol is reported + by the connections. + """ + select_args = [] + + def select(conn: Connection, options: list[bytes]) -> bytes: + select_args.append((conn, options)) + return b"spdy/2" + + client_context = Context(SSLv23_METHOD) + client_context.set_alpn_protos([b"http/1.1", b"spdy/2"]) + + server_context = Context(SSLv23_METHOD) + server_context.set_alpn_select_callback(select) + + # Necessary to actually accept the connection + server_context.use_privatekey( + load_privatekey(FILETYPE_PEM, server_key_pem) + ) + server_context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(server_context, None) + server.set_accept_state() + + client = Connection(client_context, None) + client.set_connect_state() + + interact_in_memory(server, client) + + assert select_args == [(server, [b"http/1.1", b"spdy/2"])] + + assert server.get_alpn_proto_negotiated() == b"spdy/2" + assert client.get_alpn_proto_negotiated() == b"spdy/2" + + def test_alpn_call_failure(self) -> None: + """ + SSL_CTX_set_alpn_protos does not like to be called with an empty + protocols list. Ensure that we produce a user-visible error. + """ + context = Context(SSLv23_METHOD) + with pytest.raises(ValueError): + context.set_alpn_protos([]) + + def test_alpn_set_on_connection(self) -> None: + """ + The same as test_alpn_success, but setting the ALPN protocols on + the connection rather than the context. + """ + select_args = [] + + def select(conn: Connection, options: list[bytes]) -> bytes: + select_args.append((conn, options)) + return b"spdy/2" + + # Setup the client context but don't set any ALPN protocols. + client_context = Context(SSLv23_METHOD) + + server_context = Context(SSLv23_METHOD) + server_context.set_alpn_select_callback(select) + + # Necessary to actually accept the connection + server_context.use_privatekey( + load_privatekey(FILETYPE_PEM, server_key_pem) + ) + server_context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(server_context, None) + server.set_accept_state() + + # Set the ALPN protocols on the client connection. + client = Connection(client_context, None) + client.set_alpn_protos([b"http/1.1", b"spdy/2"]) + client.set_connect_state() + + interact_in_memory(server, client) + + assert select_args == [(server, [b"http/1.1", b"spdy/2"])] + + assert server.get_alpn_proto_negotiated() == b"spdy/2" + assert client.get_alpn_proto_negotiated() == b"spdy/2" + + def test_alpn_server_fail(self) -> None: + """ + When clients and servers cannot agree on what protocol to use next + the TLS connection does not get established. + """ + select_args = [] + + def select(conn: Connection, options: list[bytes]) -> bytes: + select_args.append((conn, options)) + return b"" + + client_context = Context(SSLv23_METHOD) + client_context.set_alpn_protos([b"http/1.1", b"spdy/2"]) + + server_context = Context(SSLv23_METHOD) + server_context.set_alpn_select_callback(select) + + # Necessary to actually accept the connection + server_context.use_privatekey( + load_privatekey(FILETYPE_PEM, server_key_pem) + ) + server_context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(server_context, None) + server.set_accept_state() + + client = Connection(client_context, None) + client.set_connect_state() + + # If the client doesn't return anything, the connection will fail. + with pytest.raises(Error): + interact_in_memory(server, client) + + assert select_args == [(server, [b"http/1.1", b"spdy/2"])] + + def test_alpn_no_server_overlap(self) -> None: + """ + A server can allow a TLS handshake to complete without + agreeing to an application protocol by returning + ``NO_OVERLAPPING_PROTOCOLS``. + """ + refusal_args = [] + + def refusal( + conn: Connection, options: list[bytes] + ) -> _NoOverlappingProtocols: + refusal_args.append((conn, options)) + return NO_OVERLAPPING_PROTOCOLS + + client_context = Context(SSLv23_METHOD) + client_context.set_alpn_protos([b"http/1.1", b"spdy/2"]) + + server_context = Context(SSLv23_METHOD) + server_context.set_alpn_select_callback(refusal) + + # Necessary to actually accept the connection + server_context.use_privatekey( + load_privatekey(FILETYPE_PEM, server_key_pem) + ) + server_context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(server_context, None) + server.set_accept_state() + + client = Connection(client_context, None) + client.set_connect_state() + + # Do the dance. + interact_in_memory(server, client) + + assert refusal_args == [(server, [b"http/1.1", b"spdy/2"])] + + assert client.get_alpn_proto_negotiated() == b"" + + def test_alpn_select_cb_returns_invalid_value(self) -> None: + """ + If the ALPN selection callback returns anything other than + a bytestring or ``NO_OVERLAPPING_PROTOCOLS``, a + :py:exc:`TypeError` is raised. + """ + invalid_cb_args = [] + + def invalid_cb(conn: Connection, options: list[bytes]) -> str: + invalid_cb_args.append((conn, options)) + return "can't return unicode" + + client_context = Context(SSLv23_METHOD) + client_context.set_alpn_protos([b"http/1.1", b"spdy/2"]) + + server_context = Context(SSLv23_METHOD) + server_context.set_alpn_select_callback(invalid_cb) # type: ignore[arg-type] + + # Necessary to actually accept the connection + server_context.use_privatekey( + load_privatekey(FILETYPE_PEM, server_key_pem) + ) + server_context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(server_context, None) + server.set_accept_state() + + client = Connection(client_context, None) + client.set_connect_state() + + # Do the dance. + with pytest.raises(TypeError): + interact_in_memory(server, client) + + assert invalid_cb_args == [(server, [b"http/1.1", b"spdy/2"])] + + assert client.get_alpn_proto_negotiated() == b"" + + def test_alpn_no_server(self) -> None: + """ + When clients and servers cannot agree on what protocol to use next + because the server doesn't offer ALPN, no protocol is negotiated. + """ + client_context = Context(SSLv23_METHOD) + client_context.set_alpn_protos([b"http/1.1", b"spdy/2"]) + + server_context = Context(SSLv23_METHOD) + + # Necessary to actually accept the connection + server_context.use_privatekey( + load_privatekey(FILETYPE_PEM, server_key_pem) + ) + server_context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(server_context, None) + server.set_accept_state() + + client = Connection(client_context, None) + client.set_connect_state() + + # Do the dance. + interact_in_memory(server, client) + + assert client.get_alpn_proto_negotiated() == b"" + + def test_alpn_callback_exception(self) -> None: + """ + We can handle exceptions in the ALPN select callback. + """ + select_args = [] + + def select(conn: Connection, options: list[bytes]) -> bytes: + select_args.append((conn, options)) + raise TypeError() + + client_context = Context(SSLv23_METHOD) + client_context.set_alpn_protos([b"http/1.1", b"spdy/2"]) + + server_context = Context(SSLv23_METHOD) + server_context.set_alpn_select_callback(select) + + # Necessary to actually accept the connection + server_context.use_privatekey( + load_privatekey(FILETYPE_PEM, server_key_pem) + ) + server_context.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + + # Do a little connection to trigger the logic + server = Connection(server_context, None) + server.set_accept_state() + + client = Connection(client_context, None) + client.set_connect_state() + + with pytest.raises(TypeError): + interact_in_memory(server, client) + assert select_args == [(server, [b"http/1.1", b"spdy/2"])] + + +class TestSession: + """ + Unit tests for :py:obj:`OpenSSL.SSL.Session`. + """ + + def test_construction(self) -> None: + """ + :py:class:`Session` can be constructed with no arguments, creating + a new instance of that type. + """ + new_session = Session() + assert isinstance(new_session, Session) + + +@pytest.fixture(params=["context", "connection"]) +def ctx_or_conn(request: pytest.FixtureRequest) -> Context | Connection: + ctx = Context(SSLv23_METHOD) + if request.param == "context": + return ctx + else: + return Connection(ctx, None) + + +class TestContextConnection: + """ + Unit test for methods that are exposed both by Connection and Context + objects. + """ + + def test_use_privatekey(self, ctx_or_conn: Context | Connection) -> None: + """ + `use_privatekey` takes an `OpenSSL.crypto.PKey` instance. + """ + key = PKey() + key.generate_key(TYPE_RSA, 1024) + + ctx_or_conn.use_privatekey(key) + with pytest.raises(TypeError): + ctx_or_conn.use_privatekey("") # type: ignore[arg-type] + + cryptography_key = key.to_cryptography_key() + assert isinstance(cryptography_key, rsa.RSAPrivateKey) + ctx_or_conn.use_privatekey(cryptography_key) + + def test_use_privatekey_wrong_key( + self, ctx_or_conn: Context | Connection + ) -> None: + """ + `use_privatekey` raises `OpenSSL.SSL.Error` when passed a + `OpenSSL.crypto.PKey` instance which has not been initialized. + """ + key = PKey() + key.generate_key(TYPE_RSA, 1024) + ctx_or_conn.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + with pytest.raises(Error): + ctx_or_conn.use_privatekey(key) + + def test_use_certificate(self, ctx_or_conn: Context | Connection) -> None: + """ + `use_certificate` sets the certificate which will be + used to identify connections created using the context. + """ + # TODO + # Hard to assert anything. But we could set a privatekey then ask + # OpenSSL if the cert and key agree using check_privatekey. Then as + # long as check_privatekey works right we're good... + ctx_or_conn.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + ctx_or_conn.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem).to_cryptography() + ) + + def test_use_certificate_wrong_args( + self, ctx_or_conn: Context | Connection + ) -> None: + """ + `use_certificate_wrong_args` raises `TypeError` when not passed + exactly one `OpenSSL.crypto.X509` instance as an argument. + """ + with pytest.raises(TypeError): + ctx_or_conn.use_certificate("hello, world") # type: ignore[arg-type] + + def test_use_certificate_uninitialized( + self, ctx_or_conn: Context | Connection + ) -> None: + """ + `use_certificate` raises `OpenSSL.SSL.Error` when passed a + `OpenSSL.crypto.X509` instance which has not been initialized + (ie, which does not actually have any certificate data). + """ + with pytest.raises(Error): + ctx_or_conn.use_certificate(X509()) + + +class TestConnection: + """ + Unit tests for `OpenSSL.SSL.Connection`. + """ + + # XXX get_peer_certificate -> None + # XXX sock_shutdown + # XXX master_key -> TypeError + # XXX server_random -> TypeError + # XXX connect -> TypeError + # XXX connect_ex -> TypeError + # XXX set_connect_state -> TypeError + # XXX set_accept_state -> TypeError + # XXX do_handshake -> TypeError + # XXX bio_read -> TypeError + # XXX recv -> TypeError + # XXX send -> TypeError + # XXX bio_write -> TypeError + + @pytest.mark.parametrize("bad_context", [object(), "context", None, 1]) + def test_wrong_args(self, bad_context: object) -> None: + """ + `Connection.__init__` raises `TypeError` if called with a non-`Context` + instance argument. + """ + with pytest.raises(TypeError): + Connection(bad_context) # type: ignore[arg-type] + + @pytest.mark.parametrize("bad_bio", [object(), None, 1, [1, 2, 3]]) + def test_bio_write_wrong_args(self, bad_bio: object) -> None: + """ + `Connection.bio_write` raises `TypeError` if called with a non-bytes + (or text) argument. + """ + context = Context(SSLv23_METHOD) + connection = Connection(context, None) + with pytest.raises(TypeError): + connection.bio_write(bad_bio) # type: ignore[arg-type] + + def test_bio_write(self) -> None: + """ + `Connection.bio_write` does not raise if called with bytes or + bytearray, warns if called with text. + """ + context = Context(SSLv23_METHOD) + connection = Connection(context, None) + connection.bio_write(b"xy") + connection.bio_write(bytearray(b"za")) + with pytest.warns(DeprecationWarning): + connection.bio_write("deprecated") # type: ignore[arg-type] + + def test_get_context(self) -> None: + """ + `Connection.get_context` returns the `Context` instance used to + construct the `Connection` instance. + """ + context = Context(SSLv23_METHOD) + connection = Connection(context, None) + assert connection.get_context() is context + + def test_set_context_wrong_args(self) -> None: + """ + `Connection.set_context` raises `TypeError` if called with a + non-`Context` instance argument. + """ + ctx = Context(SSLv23_METHOD) + connection = Connection(ctx, None) + with pytest.raises(TypeError): + connection.set_context(object()) # type: ignore[arg-type] + with pytest.raises(TypeError): + connection.set_context("hello") # type: ignore[arg-type] + with pytest.raises(TypeError): + connection.set_context(1) # type: ignore[arg-type] + assert ctx is connection.get_context() + + def test_set_options_wrong_args(self) -> None: + """ + `Connection.set_options` raises `TypeError` if called with + a non-`int` argument. + """ + context = Context(SSLv23_METHOD) + connection = Connection(context, None) + with pytest.raises(TypeError): + connection.set_options(None) # type: ignore[arg-type] + + def test_set_options(self) -> None: + """ + `Connection.set_options` returns the new options value. + """ + context = Context(SSLv23_METHOD) + connection = Connection(context, None) + options = connection.set_options(OP_NO_SSLv2) + assert options & OP_NO_SSLv2 == OP_NO_SSLv2 + + def test_set_context(self) -> None: + """ + `Connection.set_context` specifies a new `Context` instance to be + used for the connection. + """ + original = Context(SSLv23_METHOD) + replacement = Context(SSLv23_METHOD) + connection = Connection(original, None) + connection.set_context(replacement) + assert replacement is connection.get_context() + # Lose our references to the contexts, just in case the Connection + # isn't properly managing its own contributions to their reference + # counts. + del original, replacement + collect() + + def test_set_tlsext_host_name_wrong_args(self) -> None: + """ + If `Connection.set_tlsext_host_name` is called with a non-byte string + argument or a byte string with an embedded NUL, `TypeError` is raised. + """ + conn = Connection(Context(SSLv23_METHOD), None) + with pytest.raises(TypeError): + conn.set_tlsext_host_name(object()) # type: ignore[arg-type] + with pytest.raises(TypeError): + conn.set_tlsext_host_name(b"with\0null") + + with pytest.raises(TypeError): + conn.set_tlsext_host_name(b"example.com".decode("ascii")) # type: ignore[arg-type] + + def test_pending(self) -> None: + """ + `Connection.pending` returns the number of bytes available for + immediate read. + """ + connection = Connection(Context(SSLv23_METHOD), None) + assert connection.pending() == 0 + + def test_peek(self) -> None: + """ + `Connection.recv` peeks into the connection if `socket.MSG_PEEK` is + passed. + """ + server, client = loopback() + server.send(b"xy") + assert client.recv(2, MSG_PEEK) == b"xy" + assert client.recv(2, MSG_PEEK) == b"xy" + assert client.recv(2) == b"xy" + + def test_connect_wrong_args(self) -> None: + """ + `Connection.connect` raises `TypeError` if called with a non-address + argument. + """ + connection = Connection(Context(SSLv23_METHOD), socket_any_family()) + with pytest.raises(TypeError): + connection.connect(None) + + def test_connect_refused(self) -> None: + """ + `Connection.connect` raises `socket.error` if the underlying socket + connect method raises it. + """ + client = socket_any_family() + context = Context(SSLv23_METHOD) + clientSSL = Connection(context, client) + # pytest.raises here doesn't work because of a bug in py.test on Python + # 2.6: https://github.com/pytest-dev/pytest/issues/988 + try: + clientSSL.connect((loopback_address(client), 1)) + except OSError as e: + exc = e + assert exc.args[0] == ECONNREFUSED + + def test_connect(self) -> None: + """ + `Connection.connect` establishes a connection to the specified address. + """ + port = socket_any_family() + port.bind(("", 0)) + port.listen(3) + + clientSSL = Connection(Context(SSLv23_METHOD), socket(port.family)) + clientSSL.connect((loopback_address(port), port.getsockname()[1])) + # XXX An assertion? Or something? + + def test_connect_ex(self) -> None: + """ + If there is a connection error, `Connection.connect_ex` returns the + errno instead of raising an exception. + """ + port = socket_any_family() + port.bind(("", 0)) + port.listen(3) + + clientSSL = Connection(Context(SSLv23_METHOD), socket(port.family)) + clientSSL.setblocking(False) + result = clientSSL.connect_ex(port.getsockname()) + expected = (EINPROGRESS, EWOULDBLOCK) + assert result in expected + + def test_accept(self) -> None: + """ + `Connection.accept` accepts a pending connection attempt and returns a + tuple of a new `Connection` (the accepted client) and the address the + connection originated from. + """ + ctx = Context(SSLv23_METHOD) + ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) + port = socket_any_family() + portSSL = Connection(ctx, port) + portSSL.bind(("", 0)) + portSSL.listen(3) + + clientSSL = Connection(Context(SSLv23_METHOD), socket(port.family)) + + # Calling portSSL.getsockname() here to get the server IP address + # sounds great, but frequently fails on Windows. + clientSSL.connect((loopback_address(port), portSSL.getsockname()[1])) + + serverSSL, address = portSSL.accept() + + assert isinstance(serverSSL, Connection) + assert serverSSL.get_context() is ctx + assert address == clientSSL.getsockname() + + def test_shutdown_wrong_args(self) -> None: + """ + `Connection.set_shutdown` raises `TypeError` if called with arguments + other than integers. + """ + connection = Connection(Context(SSLv23_METHOD), None) + with pytest.raises(TypeError): + connection.set_shutdown(None) # type: ignore[arg-type] + + def test_shutdown(self) -> None: + """ + `Connection.shutdown` performs an SSL-level connection shutdown. + """ + server, client = loopback() + assert not server.shutdown() + assert server.get_shutdown() == SENT_SHUTDOWN + with pytest.raises(ZeroReturnError): + client.recv(1024) + assert client.get_shutdown() == RECEIVED_SHUTDOWN + client.shutdown() + assert client.get_shutdown() == (SENT_SHUTDOWN | RECEIVED_SHUTDOWN) + with pytest.raises(ZeroReturnError): + server.recv(1024) + assert server.get_shutdown() == (SENT_SHUTDOWN | RECEIVED_SHUTDOWN) + + def test_shutdown_closed(self) -> None: + """ + If the underlying socket is closed, `Connection.shutdown` propagates + the write error from the low level write call. + """ + server, _ = loopback() + server.sock_shutdown(2) + with pytest.raises(SysCallError) as exc: + server.shutdown() + if platform == "win32": + assert exc.value.args[0] == ESHUTDOWN + else: + assert exc.value.args[0] == EPIPE + + def test_shutdown_truncated(self) -> None: + """ + If the underlying connection is truncated, `Connection.shutdown` + raises an `Error`. + """ + server_ctx = Context(SSLv23_METHOD) + client_ctx = Context(SSLv23_METHOD) + server_ctx.use_privatekey( + load_privatekey(FILETYPE_PEM, server_key_pem) + ) + server_ctx.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + server = Connection(server_ctx, None) + client = Connection(client_ctx, None) + handshake_in_memory(client, server) + assert not server.shutdown() + with pytest.raises(WantReadError): + server.shutdown() + server.bio_shutdown() + with pytest.raises(Error): + server.shutdown() + + def test_set_shutdown(self) -> None: + """ + `Connection.set_shutdown` sets the state of the SSL connection + shutdown process. + """ + connection = Connection(Context(SSLv23_METHOD), socket_any_family()) + connection.set_shutdown(RECEIVED_SHUTDOWN) + assert connection.get_shutdown() == RECEIVED_SHUTDOWN + + def test_state_string(self) -> None: + """ + `Connection.state_string` verbosely describes the current state of + the `Connection`. + """ + server, client = socket_pair() + tls_server = loopback_server_factory(server) + tls_client = loopback_client_factory(client) + + assert tls_server.get_state_string() in [ + b"before/accept initialization", + b"before SSL initialization", + b"TLS server start_accept", + ] + assert tls_client.get_state_string() in [ + b"before/connect initialization", + b"before SSL initialization", + b"TLS client start_connect", + ] + + def test_app_data(self) -> None: + """ + Any object can be set as app data by passing it to + `Connection.set_app_data` and later retrieved with + `Connection.get_app_data`. + """ + conn = Connection(Context(SSLv23_METHOD), None) + assert None is conn.get_app_data() + app_data = object() + conn.set_app_data(app_data) + assert conn.get_app_data() is app_data + + def test_makefile(self) -> None: + """ + `Connection.makefile` is not implemented and calling that + method raises `NotImplementedError`. + """ + conn = Connection(Context(SSLv23_METHOD), None) + with pytest.raises(NotImplementedError): + conn.makefile() + + def test_get_certificate(self) -> None: + """ + `Connection.get_certificate` returns the local certificate. + """ + [_, _, (_, scert)] = _create_certificate_chain() + + context = Context(SSLv23_METHOD) + context.use_certificate(scert) + client = Connection(context, None) + cert = client.get_certificate() + assert cert is not None + assert "Server Certificate" == cert.get_subject().CN + + cryptography_cert = client.get_certificate(as_cryptography=True) + assert cryptography_cert is not None + assert ( + cryptography_cert.subject.rfc4514_string() + == "CN=Server Certificate" + ) + + def test_get_certificate_none(self) -> None: + """ + `Connection.get_certificate` returns the local certificate. + + If there is no certificate, it returns None. + """ + context = Context(SSLv23_METHOD) + client = Connection(context, None) + cert = client.get_certificate() + assert cert is None + + def test_get_peer_cert_chain(self) -> None: + """ + `Connection.get_peer_cert_chain` returns a list of certificates + which the connected server returned for the certification verification. + """ + [(_, cacert), (_, icert), (skey, scert)] = _create_certificate_chain() + + serverContext = Context(SSLv23_METHOD) + serverContext.use_privatekey(skey) + serverContext.use_certificate(scert) + serverContext.add_extra_chain_cert(icert) + serverContext.add_extra_chain_cert(cacert.to_cryptography()) + server = Connection(serverContext, None) + server.set_accept_state() + + # Create the client + clientContext = Context(SSLv23_METHOD) + clientContext.set_verify(VERIFY_NONE, verify_cb) + client = Connection(clientContext, None) + client.set_connect_state() + + interact_in_memory(client, server) + + chain = client.get_peer_cert_chain() + assert chain is not None + assert len(chain) == 3 + assert "Server Certificate" == chain[0].get_subject().CN + assert "Intermediate Certificate" == chain[1].get_subject().CN + assert "Authority Certificate" == chain[2].get_subject().CN + + cryptography_chain = client.get_peer_cert_chain(as_cryptography=True) + assert cryptography_chain is not None + assert len(cryptography_chain) == 3 + assert ( + cryptography_chain[0].subject.rfc4514_string() + == "CN=Server Certificate" + ) + assert ( + cryptography_chain[1].subject.rfc4514_string() + == "CN=Intermediate Certificate" + ) + assert ( + cryptography_chain[2].subject.rfc4514_string() + == "CN=Authority Certificate" + ) + + def test_get_peer_cert_chain_none(self) -> None: + """ + `Connection.get_peer_cert_chain` returns `None` if the peer sends + no certificate chain. + """ + ctx = Context(SSLv23_METHOD) + ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) + server = Connection(ctx, None) + server.set_accept_state() + client = Connection(Context(SSLv23_METHOD), None) + client.set_connect_state() + interact_in_memory(client, server) + assert None is server.get_peer_cert_chain() + + def test_get_verified_chain(self) -> None: + """ + `Connection.get_verified_chain` returns a list of certificates + which the connected server returned for the certification verification. + """ + [(_, cacert), (_, icert), (skey, scert)] = _create_certificate_chain() + + serverContext = Context(SSLv23_METHOD) + serverContext.use_privatekey(skey) + serverContext.use_certificate(scert) + serverContext.add_extra_chain_cert(icert.to_cryptography()) + serverContext.add_extra_chain_cert(cacert) + server = Connection(serverContext, None) + server.set_accept_state() + + # Create the client + clientContext = Context(SSLv23_METHOD) + # cacert is self-signed so the client must trust it for verification + # to succeed. + cert_store = clientContext.get_cert_store() + assert cert_store is not None + cert_store.add_cert(cacert) + clientContext.set_verify(VERIFY_PEER, verify_cb) + client = Connection(clientContext, None) + client.set_connect_state() + + interact_in_memory(client, server) + + chain = client.get_verified_chain() + assert chain is not None + assert len(chain) == 3 + assert "Server Certificate" == chain[0].get_subject().CN + assert "Intermediate Certificate" == chain[1].get_subject().CN + assert "Authority Certificate" == chain[2].get_subject().CN + + cryptography_chain = client.get_verified_chain(as_cryptography=True) + assert cryptography_chain is not None + assert len(cryptography_chain) == 3 + assert ( + cryptography_chain[0].subject.rfc4514_string() + == "CN=Server Certificate" + ) + assert ( + cryptography_chain[1].subject.rfc4514_string() + == "CN=Intermediate Certificate" + ) + assert ( + cryptography_chain[2].subject.rfc4514_string() + == "CN=Authority Certificate" + ) + + def test_get_verified_chain_none(self) -> None: + """ + `Connection.get_verified_chain` returns `None` if the peer sends + no certificate chain. + """ + ctx = Context(SSLv23_METHOD) + ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) + server = Connection(ctx, None) + server.set_accept_state() + client = Connection(Context(SSLv23_METHOD), None) + client.set_connect_state() + interact_in_memory(client, server) + assert None is server.get_verified_chain() + + def test_get_verified_chain_unconnected(self) -> None: + """ + `Connection.get_verified_chain` returns `None` when used with an object + which has not been connected. + """ + ctx = Context(SSLv23_METHOD) + server = Connection(ctx, None) + assert None is server.get_verified_chain() + + def test_set_verify_overrides_context(self) -> None: + context = Context(SSLv23_METHOD) + context.set_verify(VERIFY_PEER) + conn = Connection(context, None) + conn.set_verify(VERIFY_NONE) + + assert context.get_verify_mode() == VERIFY_PEER + assert conn.get_verify_mode() == VERIFY_NONE + + with pytest.raises(TypeError): + conn.set_verify(None) # type: ignore[arg-type] + + with pytest.raises(TypeError): + conn.set_verify(VERIFY_PEER, "not a callable") # type: ignore[arg-type] + + def test_set_verify_callback_reference(self) -> None: + """ + The callback for certificate verification should only be forgotten if + the context and all connections created by it do not use it anymore. + """ + + def callback( + conn: Connection, cert: X509, errnum: int, depth: int, ok: int + ) -> bool: # pragma: no cover + return bool(ok) + + tracker = ref(callback) + + context = Context(SSLv23_METHOD) + context.set_verify(VERIFY_PEER, callback) + del callback + + conn = Connection(context, None) + + collect() + collect() + assert tracker() + + # Setting a new callback on the connection should maintain the original + # context callback reference + conn.set_verify( + VERIFY_PEER, lambda conn, cert, errnum, depth, ok: bool(ok) + ) + collect() + collect() + + # The callback should still be referenced - check that it exists + callback_ref = tracker() + assert callback_ref is not None + + def test_get_session_unconnected(self) -> None: + """ + `Connection.get_session` returns `None` when used with an object + which has not been connected. + """ + ctx = Context(SSLv23_METHOD) + server = Connection(ctx, None) + session = server.get_session() + assert None is session + + def test_server_get_session(self) -> None: + """ + On the server side of a connection, `Connection.get_session` returns a + `Session` instance representing the SSL session for that connection. + """ + server, _ = loopback() + session = server.get_session() + assert isinstance(session, Session) + + def test_client_get_session(self) -> None: + """ + On the client side of a connection, `Connection.get_session` + returns a `Session` instance representing the SSL session for + that connection. + """ + _, client = loopback() + session = client.get_session() + assert isinstance(session, Session) + + def test_set_session_wrong_args(self) -> None: + """ + `Connection.set_session` raises `TypeError` if called with an object + that is not an instance of `Session`. + """ + ctx = Context(SSLv23_METHOD) + connection = Connection(ctx, None) + with pytest.raises(TypeError): + connection.set_session(123) # type: ignore[arg-type] + with pytest.raises(TypeError): + connection.set_session("hello") # type: ignore[arg-type] + with pytest.raises(TypeError): + connection.set_session(object()) # type: ignore[arg-type] + + def test_client_set_session(self) -> None: + """ + `Connection.set_session`, when used prior to a connection being + established, accepts a `Session` instance and causes an attempt to + re-use the session it represents when the SSL handshake is performed. + """ + key = load_privatekey(FILETYPE_PEM, server_key_pem) + cert = load_certificate(FILETYPE_PEM, server_cert_pem) + ctx = Context(TLSv1_2_METHOD) + ctx.use_privatekey(key) + ctx.use_certificate(cert) + ctx.set_session_id(b"unity-test") + + def makeServer(socket: socket) -> Connection: + server = Connection(ctx, socket) + server.set_accept_state() + return server + + clientCtx = Context(SSLv23_METHOD) + + def makeOriginalClient(socket: socket) -> Connection: + client = Connection(clientCtx, socket) + client.set_connect_state() + return client + + originalServer, originalClient = loopback( + server_factory=makeServer, client_factory=makeOriginalClient + ) + originalSession = originalClient.get_session() + assert originalSession is not None + + def makeClient(socket: socket) -> Connection: + client = Connection(clientCtx, socket) + client.set_connect_state() + client.set_session(originalSession) + return client + + resumedServer, _ = loopback( + server_factory=makeServer, client_factory=makeClient + ) + + # This is a proxy: in general, we have no access to any unique + # identifier for the session (new enough versions of OpenSSL expose + # a hash which could be usable, but "new enough" is very, very new). + # Instead, exploit the fact that the master key is re-used if the + # session is re-used. As long as the master key for the two + # connections is the same, the session was re-used! + assert originalServer.master_key() == resumedServer.master_key() + + def test_set_session_wrong_context(self) -> None: + """ + If `Connection.set_session` is passed a `Session` instance that was + created by a `Connection` using a different `Context` than the + `Connection` is using, a `ValueError` is raised. + """ + key = load_privatekey(FILETYPE_PEM, server_key_pem) + cert = load_certificate(FILETYPE_PEM, server_cert_pem) + ctx = Context(TLSv1_2_METHOD) + ctx.use_privatekey(key) + ctx.use_certificate(cert) + ctx.set_session_id(b"unity-test") + + def makeServer(socket: socket) -> Connection: + server = Connection(ctx, socket) + server.set_accept_state() + return server + + _, originalClient = loopback(server_factory=makeServer) + originalSession = originalClient.get_session() + assert originalSession is not None + + # Intentionally use a different Context here. + client = Connection(Context(SSLv23_METHOD), None) + client.set_connect_state() + with pytest.raises(ValueError): + client.set_session(originalSession) + + def test_wantWriteError(self) -> None: + """ + `Connection` methods which generate output raise + `OpenSSL.SSL.WantWriteError` if writing to the connection's BIO + fail indicating a should-write state. + """ + # Use Unix domain sockets rather than TCP loopback. On macOS, + # TCP loopback aggressively auto-tunes buffer sizes and drains + # the send buffer into the peer's receive buffer nearly + # instantly, so the send buffer won't stay full long enough + # for do_handshake() to observe it. + client_socket, peer = socketpair() + client_socket.setblocking(False) + peer.setblocking(False) + # Fill up the client's send buffer so Connection won't be able to write + # anything. Start by sending larger chunks (Windows Socket I/O is slow) + # and continue by writing a single byte at a time so we can be sure we + # completely fill the buffer. Even though the socket API is allowed to + # signal a short write via its return value it seems this doesn't + # always happen on all platforms (FreeBSD and OS X particular) for the + # very last bit of available buffer space. + for msg in [b"x" * 65536, b"x"]: + for i in range(1024 * 1024 * 64): + try: + client_socket.send(msg) + except OSError as e: + if e.errno == EWOULDBLOCK: + break + raise # pragma: no cover + else: # pragma: no cover + pytest.fail( + "Failed to fill socket buffer, cannot test BIO want write" + ) + + ctx = Context(SSLv23_METHOD) + conn = Connection(ctx, client_socket) + # Client's speak first, so make it an SSL client + conn.set_connect_state() + with pytest.raises(WantWriteError): + conn.do_handshake() + + # XXX want_read + + def _attempt_want_write_error( + self, client: Connection, buffer_size: int + ) -> bytes: + """ + Deliberately attempts to send application data + over SSL to trigger WantWriteError. The send may need + to be repeated many times depending on the socket and + network buffer sizes allocated by the environment. + Returns the message that triggered the error so that + the buffer for the message is not immediately reclaimed. + """ + initial_want_write_triggered = False + max_num_of_attempts = 100000 + + for i in range(max_num_of_attempts): + msg = b"Y" * buffer_size + try: + client.send(msg) + except SSL.WantWriteError: + initial_want_write_triggered = True + break # Exit loop as desired error was triggered + + assert initial_want_write_triggered, ( + f"Could not induce WantWriteError within {i + 1} attempts" + ) + return msg + + def _drain_server_buffers(self, server: Connection) -> None: + """Reads from server SSL and raw sockets to drain any pending data.""" + total_ssl_read = 0 + consecutive_empty_ssl_reads = 0 + + while total_ssl_read < 1024 * 1024: + try: + data = server.recv(65536) + # if serverbuffer is empty the call should + # raise WantReadError not return None + assert data is not None, "SSL peer closed or empty data" + total_ssl_read += len(data) + # Reset counter on successful read + consecutive_empty_ssl_reads = 0 + except SSL.WantReadError: + consecutive_empty_ssl_reads += 1 + if consecutive_empty_ssl_reads >= 10: + # "No more SSL application data available after + # consecutive_empty_ssl_readss + return + # Small delay to allow time for clearing buffers + time.sleep(0.01) + + def _perform_moving_buffer_test( + self, client: Connection, buffer_size: int, want_bad_retry: bool + ) -> bool: + """ + Attempts a retry write with a moving buffer and checks for + 'bad write retry' error. + Returns True if 'bad write retry' occurs, False otherwise. + """ + # Attempt retry with different buffer but same size + msg2 = b"Z" * buffer_size + try: + bytes_written = client.send(msg2) + assert not want_bad_retry, ( + "_perform_moving_buffer_test() failed as retry succeeded " + f"unexpectedly with {bytes_written} bytes written." + ) + return False # Retry succeeded + except SSL.Error as e: + reason = get_ssl_error_reason(e) + assert reason in ( + "bad write retry", + "BAD_WRITE_RETRY", + ), f"Retry failed with unexpected SSL error: {e!r}({reason})." + return True # Bad write retry + + def _shutdown_connections( + self, + client: Connection, + server: Connection, + ) -> None: + """Helper to safely shut down SSL connections and close sockets.""" + if client: + with contextlib.suppress(SSL.Error): + # When closing connections in the test teardown stage, + # we don't care about possible TLS-level problems as the test + # was specifically emulating corner case situations + # pre-shutdown. We just attempt releasing resources + # if possible and disregard any possibly related + # problems that may occur at this point. + client.shutdown() + if server: + with contextlib.suppress(SSL.Error): + server.shutdown() + + @pytest.fixture + def ssl_connection_setup( + self, request: pytest.FixtureRequest + ) -> typing.Generator[ + tuple[Connection, Connection, int, bool], + None, + None, + ]: + """ + Sets up a non-blocking SSL connection for testing + bad_write_retry errors. + Modeflag allows the caller to turn off + SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER which is normally + on by default. + """ + want_bad_retry = request.param.get("want_bad_retry") + request_buffer_size = request.param.get("request_buffer_size") + modeflag = request.param.get("modeflag") + + client, server, sndbuf, rcvbuf = create_ssl_nonblocking_connection( + modeflag, request_buffer_size + ) + # Use a buffer size that is half the size + # of the allocated socket buffers + buffer_size = min(sndbuf, rcvbuf) // 2 + + # Yield the resources needed by the test + yield ( + client, + server, + buffer_size, + want_bad_retry, + ) + + # Teardown: Clean up the connections after the test finishes + self._shutdown_connections(client, server) + + @pytest.mark.parametrize( + "ssl_connection_setup", + [ + { + "request_buffer_size": 65536, + "modeflag": _lib.SSL_MODE_ENABLE_PARTIAL_WRITE, + "want_bad_retry": True, + }, + { + "request_buffer_size": 65536, + "modeflag": None, + "want_bad_retry": False, + }, + ], + indirect=True, + ) + def test_moving_buffer_behavior( + self, + ssl_connection_setup: tuple[Connection, Connection, int, bool], + ) -> None: + """Tests for possible "bad write retry" errors over an SSL connection. + If an SSL connection partially processes some data, + and then hits an `OpenSSL.SSL.WantWriteError`, + the connection may expect a retry. When PyOpenSSL creates + a new connection object, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is + applied by default. This mode allows for data to be sent from a + different buffer location, something that may happen if Python moves a + mutable object such as a bytearray as part of its memory management. + If the mode is turned off, OpenSSL will reject the resend with + "bad_write_retry" error. + """ + ( + client, + server, + buffer_size, + want_bad_retry, + ) = ssl_connection_setup + + _ = self._attempt_want_write_error(client, buffer_size) + self._drain_server_buffers(server) + + # Perform the test and get the result + result = self._perform_moving_buffer_test( + client, buffer_size, want_bad_retry + ) + + # Assert that the result matches the expected outcome from the fixture + assert result == want_bad_retry + + def test_get_finished_before_connect(self) -> None: + """ + `Connection.get_finished` returns `None` before TLS handshake + is completed. + """ + ctx = Context(SSLv23_METHOD) + connection = Connection(ctx, None) + assert connection.get_finished() is None + + def test_get_peer_finished_before_connect(self) -> None: + """ + `Connection.get_peer_finished` returns `None` before TLS handshake + is completed. + """ + ctx = Context(SSLv23_METHOD) + connection = Connection(ctx, None) + assert connection.get_peer_finished() is None + + def test_get_finished(self) -> None: + """ + `Connection.get_finished` method returns the TLS Finished message send + from client, or server. Finished messages are send during + TLS handshake. + """ + server, _ = loopback() + + finished = server.get_finished() + assert finished is not None + assert len(finished) > 0 + + def test_get_peer_finished(self) -> None: + """ + `Connection.get_peer_finished` method returns the TLS Finished + message received from client, or server. Finished messages are send + during TLS handshake. + """ + server, _ = loopback() + + finished = server.get_peer_finished() + assert finished is not None + assert len(finished) > 0 + + def test_tls_finished_message_symmetry(self) -> None: + """ + The TLS Finished message send by server must be the TLS Finished + message received by client. + + The TLS Finished message send by client must be the TLS Finished + message received by server. + """ + server, client = loopback() + + assert server.get_finished() == client.get_peer_finished() + assert client.get_finished() == server.get_peer_finished() + + def test_get_cipher_name_before_connect(self) -> None: + """ + `Connection.get_cipher_name` returns `None` if no connection + has been established. + """ + ctx = Context(SSLv23_METHOD) + conn = Connection(ctx, None) + assert conn.get_cipher_name() is None + + def test_get_cipher_name(self) -> None: + """ + `Connection.get_cipher_name` returns a `unicode` string giving the + name of the currently used cipher. + """ + server, client = loopback() + server_cipher_name, client_cipher_name = ( + server.get_cipher_name(), + client.get_cipher_name(), + ) + + assert isinstance(server_cipher_name, str) + assert isinstance(client_cipher_name, str) + + assert server_cipher_name == client_cipher_name + + def test_get_cipher_version_before_connect(self) -> None: + """ + `Connection.get_cipher_version` returns `None` if no connection + has been established. + """ + ctx = Context(SSLv23_METHOD) + conn = Connection(ctx, None) + assert conn.get_cipher_version() is None + + def test_get_cipher_version(self) -> None: + """ + `Connection.get_cipher_version` returns a `unicode` string giving + the protocol name of the currently used cipher. + """ + server, client = loopback() + server_cipher_version, client_cipher_version = ( + server.get_cipher_version(), + client.get_cipher_version(), + ) + + assert isinstance(server_cipher_version, str) + assert isinstance(client_cipher_version, str) + + assert server_cipher_version == client_cipher_version + + def test_get_cipher_bits_before_connect(self) -> None: + """ + `Connection.get_cipher_bits` returns `None` if no connection has + been established. + """ + ctx = Context(SSLv23_METHOD) + conn = Connection(ctx, None) + assert conn.get_cipher_bits() is None + + def test_get_cipher_bits(self) -> None: + """ + `Connection.get_cipher_bits` returns the number of secret bits + of the currently used cipher. + """ + server, client = loopback() + server_cipher_bits, client_cipher_bits = ( + server.get_cipher_bits(), + client.get_cipher_bits(), + ) + + assert isinstance(server_cipher_bits, int) + assert isinstance(client_cipher_bits, int) + + assert server_cipher_bits == client_cipher_bits + + def test_get_protocol_version_name(self) -> None: + """ + `Connection.get_protocol_version_name()` returns a string giving the + protocol version of the current connection. + """ + server, client = loopback() + client_protocol_version_name = client.get_protocol_version_name() + server_protocol_version_name = server.get_protocol_version_name() + + assert isinstance(server_protocol_version_name, str) + assert isinstance(client_protocol_version_name, str) + + assert server_protocol_version_name == client_protocol_version_name + + def test_get_protocol_version(self) -> None: + """ + `Connection.get_protocol_version()` returns an integer + giving the protocol version of the current connection. + """ + server, client = loopback() + client_protocol_version = client.get_protocol_version() + server_protocol_version = server.get_protocol_version() + + assert isinstance(server_protocol_version, int) + assert isinstance(client_protocol_version, int) + + assert server_protocol_version == client_protocol_version + + @pytest.mark.skipif( + not getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", None), + reason="SSL_get0_group_name unavailable", + ) + def test_get_group_name_before_connect(self) -> None: + """ + `Connection.get_group_name()` returns `None` if no connection + has been established. + """ + ctx = Context(TLS_METHOD) + conn = Connection(ctx, None) + assert conn.get_group_name() is None + + @pytest.mark.skipif( + not getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", None), + reason="SSL_get0_group_name unavailable", + ) + def test_group_name_null_case( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """ + `Connection.get_group_name()` returns `None` when SSL_get0_group_name + returns NULL. + """ + monkeypatch.setattr(_lib, "SSL_get0_group_name", lambda ssl: _ffi.NULL) + + server, client = loopback() + assert server.get_group_name() is None + assert client.get_group_name() is None + + @pytest.mark.skipif( + not getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", None), + reason="SSL_get0_group_name unavailable", + ) + def test_get_group_name(self) -> None: + """ + `Connection.get_group_name()` returns a string giving the + name of the connection's negotiated key exchange group. + """ + server, client = loopback() + server_group_name = server.get_group_name() + client_group_name = client.get_group_name() + + assert isinstance(server_group_name, str) + assert isinstance(client_group_name, str) + + assert server_group_name == client_group_name + + def test_wantReadError(self) -> None: + """ + `Connection.bio_read` raises `OpenSSL.SSL.WantReadError` if there are + no bytes available to be read from the BIO. + """ + ctx = Context(SSLv23_METHOD) + conn = Connection(ctx, None) + with pytest.raises(WantReadError): + conn.bio_read(1024) + + @pytest.mark.parametrize("bufsize", [1.0, None, object(), "bufsize"]) + def test_bio_read_wrong_args(self, bufsize: object) -> None: + """ + `Connection.bio_read` raises `TypeError` if passed a non-integer + argument. + """ + ctx = Context(SSLv23_METHOD) + conn = Connection(ctx, None) + with pytest.raises(TypeError): + conn.bio_read(bufsize) # type: ignore[arg-type] + + def test_buffer_size(self) -> None: + """ + `Connection.bio_read` accepts an integer giving the maximum number + of bytes to read and return. + """ + ctx = Context(SSLv23_METHOD) + conn = Connection(ctx, None) + conn.set_connect_state() + try: + conn.do_handshake() + except WantReadError: + pass + data = conn.bio_read(2) + assert 2 == len(data) + + def test_connection_set_info_callback(self) -> None: + (server_sock, client_sock) = socket_pair() + + context = Context(SSLv23_METHOD) + context.use_certificate(load_certificate(FILETYPE_PEM, root_cert_pem)) + context.use_privatekey(load_privatekey(FILETYPE_PEM, root_key_pem)) + server = Connection(context, server_sock) + server.set_accept_state() + + client = Connection(Context(SSLv23_METHOD), client_sock) + client.set_connect_state() + + called = [] + + def info(conn: Connection, where: int, ret: int) -> None: + assert conn is client + called.append(where) + + client.set_info_callback(info) + + handshake(client, server) + + # Verify that the callback was actually called during handshake + assert len(called) > 0 + assert SSL_CB_HANDSHAKE_START in called + assert SSL_CB_HANDSHAKE_DONE in called + + +class TestConnectionGetCipherList: + """ + Tests for `Connection.get_cipher_list`. + """ + + def test_result(self) -> None: + """ + `Connection.get_cipher_list` returns a list of `bytes` giving the + names of the ciphers which might be used. + """ + connection = Connection(Context(SSLv23_METHOD), None) + ciphers = connection.get_cipher_list() + assert isinstance(ciphers, list) + for cipher in ciphers: + assert isinstance(cipher, str) + + +class VeryLarge(bytes): + """ + Mock object so that we don't have to allocate 2**31 bytes + """ + + def __len__(self) -> int: + return 2**31 + + +class TestConnectionSend: + """ + Tests for `Connection.send`. + """ + + def test_wrong_args(self) -> None: + """ + When called with arguments other than string argument for its first + parameter, `Connection.send` raises `TypeError`. + """ + connection = Connection(Context(SSLv23_METHOD), None) + with pytest.raises(TypeError): + connection.send(object()) # type: ignore[arg-type] + with pytest.raises(TypeError): + connection.send([1, 2, 3]) # type: ignore[arg-type] + + def test_short_bytes(self) -> None: + """ + When passed a short byte string, `Connection.send` transmits all of it + and returns the number of bytes sent. + """ + server, client = loopback() + count = server.send(b"xy") + assert count == 2 + assert client.recv(2) == b"xy" + + def test_text(self) -> None: + """ + When passed a text, `Connection.send` transmits all of it and + returns the number of bytes sent. It also raises a DeprecationWarning. + """ + server, client = loopback() + with pytest.warns(DeprecationWarning) as w: + count = server.send(b"xy".decode("ascii")) # type: ignore[arg-type] + assert ( + f"{WARNING_TYPE_EXPECTED} for buf is no longer accepted, " + f"use bytes" + ) == str(w[-1].message) + assert count == 2 + assert client.recv(2) == b"xy" + + def test_short_memoryview(self) -> None: + """ + When passed a memoryview onto a small number of bytes, + `Connection.send` transmits all of them and returns the number + of bytes sent. + """ + server, client = loopback() + count = server.send(memoryview(b"xy")) + assert count == 2 + assert client.recv(2) == b"xy" + + def test_short_bytearray(self) -> None: + """ + When passed a short bytearray, `Connection.send` transmits all of + it and returns the number of bytes sent. + """ + server, client = loopback() + count = server.send(bytearray(b"xy")) + assert count == 2 + assert client.recv(2) == b"xy" + + @pytest.mark.skipif( + sys.maxsize < 2**31, + reason="sys.maxsize < 2**31 - test requires 64 bit", + ) + def test_buf_too_large(self) -> None: + """ + When passed a buffer containing >= 2**31 bytes, + `Connection.send` bails out as SSL_write only + accepts an int for the buffer length. + """ + connection = Connection(Context(SSLv23_METHOD), None) + with pytest.raises(ValueError) as exc_info: + connection.send(VeryLarge()) + exc_info.match(r"Cannot send more than .+ bytes at once") + + +def _make_memoryview(size: int) -> memoryview: + """ + Create a new ``memoryview`` wrapped around a ``bytearray`` of the given + size. + """ + return memoryview(bytearray(size)) + + +class TestConnectionRecvInto: + """ + Tests for `Connection.recv_into`. + """ + + def _no_length_test( + self, factory: typing.Callable[[int], typing.Any] + ) -> None: + """ + Assert that when the given buffer is passed to `Connection.recv_into`, + whatever bytes are available to be received that fit into that buffer + are written into that buffer. + """ + output_buffer = factory(5) + + server, client = loopback() + server.send(b"xy") + + assert client.recv_into(output_buffer) == 2 + assert output_buffer == bytearray(b"xy\x00\x00\x00") + + def test_bytearray_no_length(self) -> None: + """ + `Connection.recv_into` can be passed a `bytearray` instance and data + in the receive buffer is written to it. + """ + self._no_length_test(bytearray) + + def _respects_length_test( + self, factory: typing.Callable[[int], typing.Any] + ) -> None: + """ + Assert that when the given buffer is passed to `Connection.recv_into` + along with a value for `nbytes` that is less than the size of that + buffer, only `nbytes` bytes are written into the buffer. + """ + output_buffer = factory(10) + + server, client = loopback() + server.send(b"abcdefghij") + + assert client.recv_into(output_buffer, 5) == 5 + assert output_buffer == bytearray(b"abcde\x00\x00\x00\x00\x00") + + def test_bytearray_respects_length(self) -> None: + """ + When called with a `bytearray` instance, `Connection.recv_into` + respects the `nbytes` parameter and doesn't copy in more than that + number of bytes. + """ + self._respects_length_test(bytearray) + + def _doesnt_overfill_test( + self, factory: typing.Callable[[int], typing.Any] + ) -> None: + """ + Assert that if there are more bytes available to be read from the + receive buffer than would fit into the buffer passed to + `Connection.recv_into`, only as many as fit are written into it. + """ + output_buffer = factory(5) + + server, client = loopback() + server.send(b"abcdefghij") + + assert client.recv_into(output_buffer) == 5 + assert output_buffer == bytearray(b"abcde") + rest = client.recv(5) + assert b"fghij" == rest + + def test_bytearray_doesnt_overfill(self) -> None: + """ + When called with a `bytearray` instance, `Connection.recv_into` + respects the size of the array and doesn't write more bytes into it + than will fit. + """ + self._doesnt_overfill_test(bytearray) + + def test_bytearray_really_doesnt_overfill(self) -> None: + """ + When called with a `bytearray` instance and an `nbytes` value that is + too large, `Connection.recv_into` respects the size of the array and + not the `nbytes` value and doesn't write more bytes into the buffer + than will fit. + """ + self._doesnt_overfill_test(bytearray) + + def test_peek(self) -> None: + server, client = loopback() + server.send(b"xy") + + for _ in range(2): + output_buffer = bytearray(5) + assert client.recv_into(output_buffer, flags=MSG_PEEK) == 2 + assert output_buffer == bytearray(b"xy\x00\x00\x00") + + def test_memoryview_no_length(self) -> None: + """ + `Connection.recv_into` can be passed a `memoryview` instance and data + in the receive buffer is written to it. + """ + self._no_length_test(_make_memoryview) + + def test_memoryview_respects_length(self) -> None: + """ + When called with a `memoryview` instance, `Connection.recv_into` + respects the ``nbytes`` parameter and doesn't copy more than that + number of bytes in. + """ + self._respects_length_test(_make_memoryview) + + def test_memoryview_doesnt_overfill(self) -> None: + """ + When called with a `memoryview` instance, `Connection.recv_into` + respects the size of the array and doesn't write more bytes into it + than will fit. + """ + self._doesnt_overfill_test(_make_memoryview) + + def test_memoryview_really_doesnt_overfill(self) -> None: + """ + When called with a `memoryview` instance and an `nbytes` value that is + too large, `Connection.recv_into` respects the size of the array and + not the `nbytes` value and doesn't write more bytes into the buffer + than will fit. + """ + self._doesnt_overfill_test(_make_memoryview) + + +class TestConnectionSendall: + """ + Tests for `Connection.sendall`. + """ + + def test_wrong_args(self) -> None: + """ + When called with arguments other than a string argument for its first + parameter, `Connection.sendall` raises `TypeError`. + """ + connection = Connection(Context(SSLv23_METHOD), None) + with pytest.raises(TypeError): + connection.sendall(object()) # type: ignore[arg-type] + with pytest.raises(TypeError): + connection.sendall([1, 2, 3]) # type: ignore[arg-type] + + def test_short(self) -> None: + """ + `Connection.sendall` transmits all of the bytes in the string + passed to it. + """ + server, client = loopback() + server.sendall(b"x") + assert client.recv(1) == b"x" + + def test_text(self) -> None: + """ + `Connection.sendall` transmits all the content in the string passed + to it, raising a DeprecationWarning in case of this being a text. + """ + server, client = loopback() + with pytest.warns(DeprecationWarning) as w: + server.sendall(b"x".decode("ascii")) # type: ignore[arg-type] + assert ( + f"{WARNING_TYPE_EXPECTED} for buf is no longer accepted, " + f"use bytes" + ) == str(w[-1].message) + assert client.recv(1) == b"x" + + def test_short_memoryview(self) -> None: + """ + When passed a memoryview onto a small number of bytes, + `Connection.sendall` transmits all of them. + """ + server, client = loopback() + server.sendall(memoryview(b"x")) + assert client.recv(1) == b"x" + + def test_long(self) -> None: + """ + `Connection.sendall` transmits all the bytes in the string passed to it + even if this requires multiple calls of an underlying write function. + """ + server, client = loopback() + # Should be enough, underlying SSL_write should only do 16k at a time. + # On Windows, after 32k of bytes the write will block (forever + # - because no one is yet reading). + message = b"x" * (1024 * 32 - 1) + b"y" + server.sendall(message) + accum = [] + received = 0 + while received < len(message): + data = client.recv(1024) + accum.append(data) + received += len(data) + assert message == b"".join(accum) + + def test_closed(self) -> None: + """ + If the underlying socket is closed, `Connection.sendall` propagates the + write error from the low level write call. + """ + server, _ = loopback() + server.sock_shutdown(2) + with pytest.raises(SysCallError) as err: + server.sendall(b"hello, world") + if platform == "win32": + assert err.value.args[0] == ESHUTDOWN + else: + assert err.value.args[0] == EPIPE + + +class TestConnectionRenegotiate: + """ + Tests for SSL renegotiation APIs. + """ + + def test_total_renegotiations(self) -> None: + """ + `Connection.total_renegotiations` returns `0` before any renegotiations + have happened. + """ + connection = Connection(Context(SSLv23_METHOD), None) + assert connection.total_renegotiations() == 0 + + @pytest.mark.skipif( + conftest.is_awslc, + reason="aws-lc doesn't support renegotiation", + ) + def test_renegotiate(self) -> None: + """ + Go through a complete renegotiation cycle. + """ + server, client = loopback( + lambda s: loopback_server_factory(s, TLSv1_2_METHOD), + lambda s: loopback_client_factory(s, TLSv1_2_METHOD), + ) + + server.send(b"hello world") + + assert b"hello world" == client.recv(len(b"hello world")) + + assert 0 == server.total_renegotiations() + assert False is server.renegotiate_pending() + + assert True is server.renegotiate() + + assert True is server.renegotiate_pending() + + server.setblocking(False) + client.setblocking(False) + + client.do_handshake() + server.do_handshake() + + assert 1 == server.total_renegotiations() + while False is server.renegotiate_pending(): + pass + + +class TestError: + """ + Unit tests for `OpenSSL.SSL.Error`. + """ + + def test_type(self) -> None: + """ + `Error` is an exception type. + """ + assert issubclass(Error, Exception) + assert Error.__name__ == "Error" + + +class TestConstants: + """ + Tests for the values of constants exposed in `OpenSSL.SSL`. + + These are values defined by OpenSSL intended only to be used as flags to + OpenSSL APIs. The only assertions it seems can be made about them is + their values. + """ + + @pytest.mark.skipif( + OP_NO_QUERY_MTU is None, + reason="OP_NO_QUERY_MTU unavailable - OpenSSL version may be too old", + ) + def test_op_no_query_mtu(self) -> None: + """ + The value of `OpenSSL.SSL.OP_NO_QUERY_MTU` is 0x1000, the value + of `SSL_OP_NO_QUERY_MTU` defined by `openssl/ssl.h`. + """ + assert OP_NO_QUERY_MTU == 0x1000 + + @pytest.mark.skipif( + OP_COOKIE_EXCHANGE is None, + reason="OP_COOKIE_EXCHANGE unavailable - " + "OpenSSL version may be too old", + ) + def test_op_cookie_exchange(self) -> None: + """ + The value of `OpenSSL.SSL.OP_COOKIE_EXCHANGE` is 0x2000, the + value of `SSL_OP_COOKIE_EXCHANGE` defined by `openssl/ssl.h`. + """ + assert OP_COOKIE_EXCHANGE == 0x2000 + + @pytest.mark.skipif( + OP_NO_TICKET is None, + reason="OP_NO_TICKET unavailable - OpenSSL version may be too old", + ) + def test_op_no_ticket(self) -> None: + """ + The value of `OpenSSL.SSL.OP_NO_TICKET` is 0x4000, the value of + `SSL_OP_NO_TICKET` defined by `openssl/ssl.h`. + """ + assert OP_NO_TICKET == 0x4000 + + @pytest.mark.skipif( + OP_NO_COMPRESSION is None, + reason=( + "OP_NO_COMPRESSION unavailable - OpenSSL version may be too old" + ), + ) + @pytest.mark.skipif( + conftest.is_awslc, + reason="aws-lc defines OP_NO_COMPRESSION as 0", + ) + def test_op_no_compression(self) -> None: + """ + The value of `OpenSSL.SSL.OP_NO_COMPRESSION` is 0x20000, the + value of `SSL_OP_NO_COMPRESSION` defined by `openssl/ssl.h`. + """ + assert OP_NO_COMPRESSION == 0x20000 + + def test_sess_cache_off(self) -> None: + """ + The value of `OpenSSL.SSL.SESS_CACHE_OFF` 0x0, the value of + `SSL_SESS_CACHE_OFF` defined by `openssl/ssl.h`. + """ + assert 0x0 == SESS_CACHE_OFF + + def test_sess_cache_client(self) -> None: + """ + The value of `OpenSSL.SSL.SESS_CACHE_CLIENT` 0x1, the value of + `SSL_SESS_CACHE_CLIENT` defined by `openssl/ssl.h`. + """ + assert 0x1 == SESS_CACHE_CLIENT + + def test_sess_cache_server(self) -> None: + """ + The value of `OpenSSL.SSL.SESS_CACHE_SERVER` 0x2, the value of + `SSL_SESS_CACHE_SERVER` defined by `openssl/ssl.h`. + """ + assert 0x2 == SESS_CACHE_SERVER + + def test_sess_cache_both(self) -> None: + """ + The value of `OpenSSL.SSL.SESS_CACHE_BOTH` 0x3, the value of + `SSL_SESS_CACHE_BOTH` defined by `openssl/ssl.h`. + """ + assert 0x3 == SESS_CACHE_BOTH + + def test_sess_cache_no_auto_clear(self) -> None: + """ + The value of `OpenSSL.SSL.SESS_CACHE_NO_AUTO_CLEAR` 0x80, the + value of `SSL_SESS_CACHE_NO_AUTO_CLEAR` defined by + `openssl/ssl.h`. + """ + assert 0x80 == SESS_CACHE_NO_AUTO_CLEAR + + def test_sess_cache_no_internal_lookup(self) -> None: + """ + The value of `OpenSSL.SSL.SESS_CACHE_NO_INTERNAL_LOOKUP` 0x100, + the value of `SSL_SESS_CACHE_NO_INTERNAL_LOOKUP` defined by + `openssl/ssl.h`. + """ + assert 0x100 == SESS_CACHE_NO_INTERNAL_LOOKUP + + def test_sess_cache_no_internal_store(self) -> None: + """ + The value of `OpenSSL.SSL.SESS_CACHE_NO_INTERNAL_STORE` 0x200, + the value of `SSL_SESS_CACHE_NO_INTERNAL_STORE` defined by + `openssl/ssl.h`. + """ + assert 0x200 == SESS_CACHE_NO_INTERNAL_STORE + + def test_sess_cache_no_internal(self) -> None: + """ + The value of `OpenSSL.SSL.SESS_CACHE_NO_INTERNAL` 0x300, the + value of `SSL_SESS_CACHE_NO_INTERNAL` defined by + `openssl/ssl.h`. + """ + assert 0x300 == SESS_CACHE_NO_INTERNAL + + +class TestMemoryBIO: + """ + Tests for `OpenSSL.SSL.Connection` using a memory BIO. + """ + + def _create_server_context(self) -> Context: + """ + Create a configured server context with certificates and options. + """ + server_ctx = Context(SSLv23_METHOD) + server_ctx.set_options(OP_NO_SSLv2 | OP_NO_SSLv3 | OP_SINGLE_DH_USE) + server_ctx.set_verify( + VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT | VERIFY_CLIENT_ONCE, + verify_cb, + ) + server_store = server_ctx.get_cert_store() + assert server_store is not None + server_ctx.use_privatekey( + load_privatekey(FILETYPE_PEM, server_key_pem) + ) + server_ctx.use_certificate( + load_certificate(FILETYPE_PEM, server_cert_pem) + ) + server_ctx.check_privatekey() + server_store.add_cert(load_certificate(FILETYPE_PEM, root_cert_pem)) + return server_ctx + + def _server( + self, sock: socket | None, ctx: Context | None = None + ) -> Connection: + """ + Create a new server-side SSL `Connection` object wrapped around `sock`. + + :param sock: The socket to wrap, or None for memory BIO. + :param ctx: Optional pre-configured context. If None, creates a + default server context. + """ + if ctx is None: + server_ctx = self._create_server_context() + else: + server_ctx = ctx + + # Here the Connection is actually created. If None is passed as the + # 2nd parameter, it indicates a memory BIO should be created. + server_conn = Connection(server_ctx, sock) + server_conn.set_accept_state() + return server_conn + + def _client(self, sock: socket | None) -> Connection: + """ + Create a new client-side SSL `Connection` object wrapped around `sock`. + """ + # Now create the client side Connection. Similar boilerplate to the + # above. + client_ctx = Context(SSLv23_METHOD) + client_ctx.set_options(OP_NO_SSLv2 | OP_NO_SSLv3 | OP_SINGLE_DH_USE) + client_ctx.set_verify( + VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT | VERIFY_CLIENT_ONCE, + verify_cb, + ) + client_store = client_ctx.get_cert_store() + assert client_store is not None + client_ctx.use_privatekey( + load_privatekey(FILETYPE_PEM, client_key_pem) + ) + client_ctx.use_certificate( + load_certificate(FILETYPE_PEM, client_cert_pem) + ) + client_ctx.check_privatekey() + client_store.add_cert(load_certificate(FILETYPE_PEM, root_cert_pem)) + client_conn = Connection(client_ctx, sock) + client_conn.set_connect_state() + return client_conn + + def test_memory_connect(self) -> None: + """ + Two `Connection`s which use memory BIOs can be manually connected by + reading from the output of each and writing those bytes to the input of + the other and in this way establish a connection and exchange + application-level bytes with each other. + """ + server_conn = self._server(None) + client_conn = self._client(None) + + # There should be no key or nonces yet. + assert server_conn.master_key() is None + assert server_conn.client_random() is None + assert server_conn.server_random() is None + + # First, the handshake needs to happen. We'll deliver bytes back and + # forth between the client and server until neither of them feels like + # speaking any more. + assert interact_in_memory(client_conn, server_conn) is None + + # Now that the handshake is done, there should be a key and nonces. + assert server_conn.master_key() is not None + assert server_conn.client_random() is not None + assert server_conn.server_random() is not None + assert server_conn.client_random() == client_conn.client_random() + assert server_conn.server_random() == client_conn.server_random() + assert server_conn.client_random() != server_conn.server_random() + assert client_conn.client_random() != client_conn.server_random() + + # Export key material for other uses. + cekm = client_conn.export_keying_material(b"LABEL", 32) + sekm = server_conn.export_keying_material(b"LABEL", 32) + assert cekm is not None + assert sekm is not None + assert cekm == sekm + assert len(sekm) == 32 + + # Export key material for other uses with additional context. + cekmc = client_conn.export_keying_material(b"LABEL", 32, b"CONTEXT") + sekmc = server_conn.export_keying_material(b"LABEL", 32, b"CONTEXT") + assert cekmc is not None + assert sekmc is not None + assert cekmc == sekmc + assert cekmc != cekm + assert sekmc != sekm + # Export with alternate label + cekmt = client_conn.export_keying_material(b"test", 32, b"CONTEXT") + sekmt = server_conn.export_keying_material(b"test", 32, b"CONTEXT") + assert cekmc != cekmt + assert sekmc != sekmt + + # Here are the bytes we'll try to send. + important_message = b"One if by land, two if by sea." + + server_conn.write(important_message) + assert interact_in_memory(client_conn, server_conn) == ( + client_conn, + important_message, + ) + + client_conn.write(important_message[::-1]) + assert interact_in_memory(client_conn, server_conn) == ( + server_conn, + important_message[::-1], + ) + + def test_socket_connect(self) -> None: + """ + Just like `test_memory_connect` but with an actual socket. + + This is primarily to rule out the memory BIO code as the source of any + problems encountered while passing data over a `Connection` (if + this test fails, there must be a problem outside the memory BIO code, + as no memory BIO is involved here). Even though this isn't a memory + BIO test, it's convenient to have it here. + """ + server_conn, client_conn = loopback() + + important_message = b"Help me Obi Wan Kenobi, you're my only hope." + client_conn.send(important_message) + msg = server_conn.recv(1024) + assert msg == important_message + + # Again in the other direction, just for fun. + important_message = important_message[::-1] + server_conn.send(important_message) + msg = client_conn.recv(1024) + assert msg == important_message + + def test_socket_overrides_memory(self) -> None: + """ + Test that `OpenSSL.SSL.bio_read` and `OpenSSL.SSL.bio_write` don't + work on `OpenSSL.SSL.Connection`() that use sockets. + """ + context = Context(SSLv23_METHOD) + client = socket_any_family() + clientSSL = Connection(context, client) + with pytest.raises(TypeError): + clientSSL.bio_read(100) + with pytest.raises(TypeError): + clientSSL.bio_write(b"foo") + with pytest.raises(TypeError): + clientSSL.bio_shutdown() + + def test_outgoing_overflow(self) -> None: + """ + If more bytes than can be written to the memory BIO are passed to + `Connection.send` at once, the number of bytes which were written is + returned and that many bytes from the beginning of the input can be + read from the other end of the connection. + """ + server = self._server(None) + client = self._client(None) + + interact_in_memory(client, server) + + size = 2**15 + sent = client.send(b"x" * size) + # Sanity check. We're trying to test what happens when the entire + # input can't be sent. If the entire input was sent, this test is + # meaningless. + assert sent < size + + result = interact_in_memory(client, server) + assert result is not None + receiver, received = result + assert receiver is server + + # We can rely on all of these bytes being received at once because + # loopback passes 2 ** 16 to recv - more than 2 ** 15. + assert len(received) == sent + + def test_shutdown(self) -> None: + """ + `Connection.bio_shutdown` signals the end of the data stream + from which the `Connection` reads. + """ + server = self._server(None) + server.bio_shutdown() + with pytest.raises(Error) as err: + server.recv(1024) + # We don't want WantReadError or ZeroReturnError or anything - it's a + # handshake failure. + assert type(err.value) in [Error, SysCallError] + + def test_unexpected_EOF(self) -> None: + """ + If the connection is lost before an orderly SSL shutdown occurs, + `OpenSSL.SSL.SysCallError` is raised with a message of + "Unexpected EOF" (or WSAECONNRESET on Windows). + """ + server_conn, client_conn = loopback() + client_conn.sock_shutdown(SHUT_RDWR) + with pytest.raises(SysCallError) as err: + server_conn.recv(1024) + if platform == "win32": + assert err.value.args == (10054, "WSAECONNRESET") + else: + assert err.value.args in [ + (-1, "Unexpected EOF"), + (54, "ECONNRESET"), + ] + + def _check_client_ca_list( + self, func: typing.Callable[[Context], list[X509Name]] + ) -> None: + """ + Verify the return value of the `get_client_ca_list` method for + server and client connections. + + :param func: A function which will be called with the server context + before the client and server are connected to each other. This + function should specify a list of CAs for the server to send to the + client and return that same list. The list will be used to verify + that `get_client_ca_list` returns the proper value at + various times. + """ + # Create a server context and configure it before creating connections + server_ctx = self._create_server_context() + + # Configure the CA list before creating connections + expected = func(server_ctx) + + # Now create connections with the configured context + server = self._server(None, server_ctx) + client = self._client(None) + + assert client.get_client_ca_list() == [] + assert server.get_client_ca_list() == expected + interact_in_memory(client, server) + assert client.get_client_ca_list() == expected + assert server.get_client_ca_list() == expected + + def test_set_client_ca_list_errors(self) -> None: + """ + `Context.set_client_ca_list` raises a `TypeError` if called with a + non-list or a list that contains objects other than X509Names. + """ + ctx = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + ctx.set_client_ca_list("spam") # type: ignore[arg-type] + with pytest.raises(TypeError): + ctx.set_client_ca_list(["spam"]) # type: ignore[list-item] + + def test_set_empty_ca_list(self) -> None: + """ + If passed an empty list, `Context.set_client_ca_list` configures the + context to send no CA names to the client and, on both the server and + client sides, `Connection.get_client_ca_list` returns an empty list + after the connection is set up. + """ + + def no_ca(ctx: Context) -> list[X509Name]: + ctx.set_client_ca_list([]) + return [] + + self._check_client_ca_list(no_ca) + + def test_set_one_ca_list(self) -> None: + """ + If passed a list containing a single X509Name, + `Context.set_client_ca_list` configures the context to send + that CA name to the client and, on both the server and client sides, + `Connection.get_client_ca_list` returns a list containing that + X509Name after the connection is set up. + """ + cacert = load_certificate(FILETYPE_PEM, root_cert_pem) + cadesc = cacert.get_subject() + + def single_ca(ctx: Context) -> list[X509Name]: + ctx.set_client_ca_list([cadesc]) + return [cadesc] + + self._check_client_ca_list(single_ca) + + def test_get_client_ca_list_as_cryptography(self) -> None: + """ + `Connection.get_client_ca_list` returns a list of + `cryptography.x509.Name` when called with ``as_cryptography=True``. + """ + cacert = load_certificate(FILETYPE_PEM, root_cert_pem) + expected = [cacert.to_cryptography().subject] + + server_ctx = self._create_server_context() + server_ctx.set_client_ca_list([cacert.get_subject()]) + + server = self._server(None, server_ctx) + client = self._client(None) + + assert client.get_client_ca_list(as_cryptography=True) == [] + assert server.get_client_ca_list(as_cryptography=True) == expected + interact_in_memory(client, server) + assert client.get_client_ca_list(as_cryptography=True) == expected + assert server.get_client_ca_list(as_cryptography=True) == expected + + def test_set_multiple_ca_list(self) -> None: + """ + If passed a list containing multiple X509Name objects, + `Context.set_client_ca_list` configures the context to send + those CA names to the client and, on both the server and client sides, + `Connection.get_client_ca_list` returns a list containing those + X509Names after the connection is set up. + """ + secert = load_certificate(FILETYPE_PEM, server_cert_pem) + clcert = load_certificate(FILETYPE_PEM, server_cert_pem) + + sedesc = secert.get_subject() + cldesc = clcert.get_subject() + + def multiple_ca(ctx: Context) -> list[X509Name]: + L = [sedesc, cldesc] + ctx.set_client_ca_list(L) + return L + + self._check_client_ca_list(multiple_ca) + + def test_reset_ca_list(self) -> None: + """ + If called multiple times, only the X509Names passed to the final call + of `Context.set_client_ca_list` are used to configure the CA + names sent to the client. + """ + cacert = load_certificate(FILETYPE_PEM, root_cert_pem) + secert = load_certificate(FILETYPE_PEM, server_cert_pem) + clcert = load_certificate(FILETYPE_PEM, server_cert_pem) + + cadesc = cacert.get_subject() + sedesc = secert.get_subject() + cldesc = clcert.get_subject() + + def changed_ca(ctx: Context) -> list[X509Name]: + ctx.set_client_ca_list([sedesc, cldesc]) + ctx.set_client_ca_list([cadesc]) + return [cadesc] + + self._check_client_ca_list(changed_ca) + + def test_mutated_ca_list(self) -> None: + """ + If the list passed to `Context.set_client_ca_list` is mutated + afterwards, this does not affect the list of CA names sent to the + client. + """ + cacert = load_certificate(FILETYPE_PEM, root_cert_pem) + secert = load_certificate(FILETYPE_PEM, server_cert_pem) + + cadesc = cacert.get_subject() + sedesc = secert.get_subject() + + def mutated_ca(ctx: Context) -> list[X509Name]: + L = [cadesc] + ctx.set_client_ca_list([cadesc]) + L.append(sedesc) + return [cadesc] + + self._check_client_ca_list(mutated_ca) + + def test_add_client_ca_wrong_args(self) -> None: + """ + `Context.add_client_ca` raises `TypeError` if called with + a non-X509 object. + """ + ctx = Context(SSLv23_METHOD) + with pytest.raises(TypeError): + ctx.add_client_ca("spam") # type: ignore[arg-type] + + def test_one_add_client_ca(self) -> None: + """ + A certificate's subject can be added as a CA to be sent to the client + with `Context.add_client_ca`. + """ + cacert = load_certificate(FILETYPE_PEM, root_cert_pem) + cadesc = cacert.get_subject() + + def single_ca(ctx: Context) -> list[X509Name]: + ctx.add_client_ca(cacert) + return [cadesc] + + self._check_client_ca_list(single_ca) + + def test_multiple_add_client_ca(self) -> None: + """ + Multiple CA names can be sent to the client by calling + `Context.add_client_ca` with multiple X509 objects. + """ + cacert = load_certificate(FILETYPE_PEM, root_cert_pem) + secert = load_certificate(FILETYPE_PEM, server_cert_pem) + + cadesc = cacert.get_subject() + sedesc = secert.get_subject() + + def multiple_ca(ctx: Context) -> list[X509Name]: + ctx.add_client_ca(cacert) + ctx.add_client_ca(secert.to_cryptography()) + return [cadesc, sedesc] + + self._check_client_ca_list(multiple_ca) + + def test_set_and_add_client_ca(self) -> None: + """ + A call to `Context.set_client_ca_list` followed by a call to + `Context.add_client_ca` results in using the CA names from the + first call and the CA name from the second call. + """ + cacert = load_certificate(FILETYPE_PEM, root_cert_pem) + secert = load_certificate(FILETYPE_PEM, server_cert_pem) + clcert = load_certificate(FILETYPE_PEM, server_cert_pem) + + cadesc = cacert.get_subject() + sedesc = secert.get_subject() + cldesc = clcert.get_subject() + + def mixed_set_add_ca(ctx: Context) -> list[X509Name]: + ctx.set_client_ca_list([cadesc, sedesc]) + ctx.add_client_ca(clcert) + return [cadesc, sedesc, cldesc] + + self._check_client_ca_list(mixed_set_add_ca) + + def test_set_after_add_client_ca(self) -> None: + """ + A call to `Context.set_client_ca_list` after a call to + `Context.add_client_ca` replaces the CA name specified by the + former call with the names specified by the latter call. + """ + cacert = load_certificate(FILETYPE_PEM, root_cert_pem) + secert = load_certificate(FILETYPE_PEM, server_cert_pem) + clcert = load_certificate(FILETYPE_PEM, server_cert_pem) + + cadesc = cacert.get_subject() + sedesc = secert.get_subject() + + def set_replaces_add_ca(ctx: Context) -> list[X509Name]: + ctx.add_client_ca(clcert.to_cryptography()) + ctx.set_client_ca_list([cadesc]) + ctx.add_client_ca(secert) + return [cadesc, sedesc] + + self._check_client_ca_list(set_replaces_add_ca) + + +class TestInfoConstants: + """ + Tests for assorted constants exposed for use in info callbacks. + """ + + def test_integers(self) -> None: + """ + All of the info constants are integers. + + This is a very weak test. It would be nice to have one that actually + verifies that as certain info events happen, the value passed to the + info callback matches up with the constant exposed by OpenSSL.SSL. + """ + for const in [ + SSL_ST_CONNECT, + SSL_ST_ACCEPT, + SSL_ST_MASK, + SSL_CB_LOOP, + SSL_CB_EXIT, + SSL_CB_READ, + SSL_CB_WRITE, + SSL_CB_ALERT, + SSL_CB_READ_ALERT, + SSL_CB_WRITE_ALERT, + SSL_CB_ACCEPT_LOOP, + SSL_CB_ACCEPT_EXIT, + SSL_CB_CONNECT_LOOP, + SSL_CB_CONNECT_EXIT, + SSL_CB_HANDSHAKE_START, + SSL_CB_HANDSHAKE_DONE, + ]: + assert isinstance(const, int) + + +class TestRequires: + """ + Tests for the decorator factory used to conditionally raise + NotImplementedError when older OpenSSLs are used. + """ + + def test_available(self) -> None: + """ + When the OpenSSL functionality is available the decorated functions + work appropriately. + """ + feature_guard = _make_requires(True, "Error text") + results = [] + + @feature_guard + def inner() -> bool: + results.append(True) + return True + + assert inner() is True + assert [True] == results + + def test_unavailable(self) -> None: + """ + When the OpenSSL functionality is not available the decorated function + does not execute and NotImplementedError is raised. + """ + feature_guard = _make_requires(False, "Error text") + + @feature_guard + def inner() -> None: # pragma: nocover + pytest.fail("Should not be called") + + with pytest.raises(NotImplementedError) as e: + inner() + + assert "Error text" in str(e.value) + + +T = typing.TypeVar("T") + + +class TestOCSP: + """ + Tests for PyOpenSSL's OCSP stapling support. + """ + + # Minimal valid DER-encoded OCSPResponse with status "unauthorized" + # (SEQUENCE { ENUMERATED 6 }). Required by OpenSSL 4.0+, which parses + # the bytes via d2i_OCSP_RESPONSE before stapling and silently drops + # unparseable input. + sample_ocsp_data = b"\x30\x03\x0a\x01\x06" + + def _client_connection( + self, + callback: typing.Callable[[Connection, bytes, T | None], bool], + data: T | None, + request_ocsp: bool = True, + ) -> Connection: + """ + Builds a client connection suitable for using OCSP. + + :param callback: The callback to register for OCSP. + :param data: The opaque data object that will be handed to the + OCSP callback. + :param request_ocsp: Whether the client will actually ask for OCSP + stapling. Useful for testing only. + """ + ctx = Context(SSLv23_METHOD) + ctx.set_ocsp_client_callback(callback, data) + client = Connection(ctx) + + if request_ocsp: + client.request_ocsp() + + client.set_connect_state() + return client + + def _server_connection( + self, + callback: typing.Callable[[Connection, T | None], bytes], + data: T | None, + ) -> Connection: + """ + Builds a server connection suitable for using OCSP. + + :param callback: The callback to register for OCSP. + :param data: The opaque data object that will be handed to the + OCSP callback. + """ + ctx = Context(SSLv23_METHOD) + ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) + ctx.set_ocsp_server_callback(callback, data) + server = Connection(ctx) + server.set_accept_state() + return server + + def test_callbacks_arent_called_by_default(self) -> None: + """ + If both the client and the server have registered OCSP callbacks, but + the client does not send the OCSP request, neither callback gets + called. + """ + + def ocsp_callback(*args: object) -> typing.NoReturn: # pragma: nocover + pytest.fail("Should not be called") + + client = self._client_connection( + callback=ocsp_callback, data=None, request_ocsp=False + ) + server = self._server_connection(callback=ocsp_callback, data=None) + handshake_in_memory(client, server) + + def test_client_negotiates_without_server(self) -> None: + """ + If the client wants to do OCSP but the server does not, the handshake + succeeds, and the client callback fires with an empty byte string. + """ + called = [] + + def ocsp_callback( + conn: Connection, ocsp_data: bytes, ignored: None + ) -> bool: + called.append(ocsp_data) + return True + + client = self._client_connection(callback=ocsp_callback, data=None) + server = loopback_server_factory(socket=None) + handshake_in_memory(client, server) + + assert len(called) == 1 + assert called[0] == b"" + + def test_client_receives_servers_data(self) -> None: + """ + The data the server sends in its callback is received by the client. + """ + calls = [] + + def server_callback(*args: object, **kwargs: object) -> bytes: + return self.sample_ocsp_data + + def client_callback( + conn: Connection, ocsp_data: bytes, ignored: None + ) -> bool: + calls.append(ocsp_data) + return True + + client = self._client_connection(callback=client_callback, data=None) + server = self._server_connection(callback=server_callback, data=None) + handshake_in_memory(client, server) + + assert len(calls) == 1 + assert calls[0] == self.sample_ocsp_data + + def test_callbacks_are_invoked_with_connections(self) -> None: + """ + The first arguments to both callbacks are their respective connections. + """ + client_calls = [] + server_calls = [] + + def client_callback( + conn: Connection, *args: object, **kwargs: object + ) -> bool: + client_calls.append(conn) + return True + + def server_callback( + conn: Connection, *args: object, **kwargs: object + ) -> bytes: + server_calls.append(conn) + return self.sample_ocsp_data + + client = self._client_connection(callback=client_callback, data=None) + server = self._server_connection(callback=server_callback, data=None) + handshake_in_memory(client, server) + + assert len(client_calls) == 1 + assert len(server_calls) == 1 + assert client_calls[0] is client + assert server_calls[0] is server + + def test_opaque_data_is_passed_through(self) -> None: + """ + Both callbacks receive an opaque, user-provided piece of data in their + callbacks as the final argument. + """ + calls = [] + + def server_callback(*args: object) -> bytes: + calls.append(args) + return self.sample_ocsp_data + + def client_callback(*args: object) -> bool: + calls.append(args) + return True + + sentinel = object() + + client = self._client_connection( + callback=client_callback, data=sentinel + ) + server = self._server_connection( + callback=server_callback, data=sentinel + ) + handshake_in_memory(client, server) + + assert len(calls) == 2 + assert calls[0][-1] is sentinel + assert calls[1][-1] is sentinel + + def test_server_returns_empty_string(self) -> None: + """ + If the server returns an empty bytestring from its callback, the + client callback is called with the empty bytestring. + """ + client_calls = [] + + def server_callback(*args: object) -> bytes: + return b"" + + def client_callback( + conn: Connection, ocsp_data: bytes, ignored: None + ) -> bool: + client_calls.append(ocsp_data) + return True + + client = self._client_connection(callback=client_callback, data=None) + server = self._server_connection(callback=server_callback, data=None) + handshake_in_memory(client, server) + + assert len(client_calls) == 1 + assert client_calls[0] == b"" + + def test_client_returns_false_terminates_handshake(self) -> None: + """ + If the client returns False from its callback, the handshake fails. + """ + + def server_callback(*args: object) -> bytes: + return self.sample_ocsp_data + + def client_callback(*args: object) -> bool: + return False + + client = self._client_connection(callback=client_callback, data=None) + server = self._server_connection(callback=server_callback, data=None) + + with pytest.raises(Error): + handshake_in_memory(client, server) + + def test_exceptions_in_client_bubble_up(self) -> None: + """ + The callbacks thrown in the client callback bubble up to the caller. + """ + + class SentinelException(Exception): + pass + + def server_callback(*args: object) -> bytes: + return self.sample_ocsp_data + + def client_callback(*args: object) -> typing.NoReturn: + raise SentinelException() + + client = self._client_connection(callback=client_callback, data=None) + server = self._server_connection(callback=server_callback, data=None) + + with pytest.raises(SentinelException): + handshake_in_memory(client, server) + + def test_exceptions_in_server_bubble_up(self) -> None: + """ + The callbacks thrown in the server callback bubble up to the caller. + """ + + class SentinelException(Exception): + pass + + def server_callback(*args: object) -> typing.NoReturn: + raise SentinelException() + + def client_callback( + *args: object, + ) -> typing.NoReturn: # pragma: nocover + pytest.fail("Should not be called") + + client = self._client_connection(callback=client_callback, data=None) + server = self._server_connection(callback=server_callback, data=None) + + with pytest.raises(SentinelException): + handshake_in_memory(client, server) + + def test_server_must_return_bytes(self) -> None: + """ + The server callback must return a bytestring, or a TypeError is thrown. + """ + + def server_callback(*args: object) -> str: + return self.sample_ocsp_data.decode("ascii") + + def client_callback( + *args: object, + ) -> typing.NoReturn: # pragma: nocover + pytest.fail("Should not be called") + + client = self._client_connection(callback=client_callback, data=None) + server = self._server_connection(callback=server_callback, data=None) # type: ignore[arg-type] + + with pytest.raises(TypeError): + handshake_in_memory(client, server) + + +class TestDTLS: + # The way you would expect DTLSv1_listen to work is: + # + # - it reads packets in a loop + # - when it finds a valid ClientHello, it returns + # - now the handshake can proceed + # + # However, on older versions of OpenSSL, it did something "cleverer". The + # way it worked is: + # + # - it "peeks" into the BIO to see the next packet without consuming it + # - if *not* a valid ClientHello, then it reads the packet to consume it + # and loops around + # - if it *is* a valid ClientHello, it *leaves the packet in the BIO*, and + # returns + # - then the handshake finds the ClientHello in the BIO and reads it a + # second time. + # + # I'm not sure exactly when this switched over. The OpenSSL v1.1.1 in + # Ubuntu 18.04 has the old behavior. The OpenSSL v1.1.1 in Ubuntu 20.04 has + # the new behavior. There doesn't seem to be any mention of this change in + # the OpenSSL v1.1.1 changelog, but presumably it changed in some point + # release or another. Presumably in 2025 or so there will be only new + # OpenSSLs around we can delete this whole comment and the weird + # workaround. If anyone is still using this library by then, which seems + # both depressing and inevitable. + # + # Anyway, why do we care? The reason is that the old strategy has a + # problem: the "peek" operation is only defined on "DGRAM BIOs", which are + # a special type of object that is different from the more familiar "socket + # BIOs" and "memory BIOs". If you *don't* have a DGRAM BIO, and you try to + # peek into the BIO... then it silently degrades to a full-fledged "read" + # operation that consumes the packet. Which is a problem if your algorithm + # depends on leaving the packet in the BIO to be read again later. + # + # So on old OpenSSL, we have a problem: + # + # - we can't use a DGRAM BIO, because cryptography/pyopenssl don't wrap the + # relevant APIs, nor should they. + # + # - if we use a socket BIO, then the first time DTLSv1_listen sees an + # invalid packet (like for example... the challenge packet that *every + # DTLS handshake starts with before the real ClientHello!*), it tries to + # first "peek" it, and then "read" it. But since the first "peek" + # consumes the packet, the second "read" ends up hanging or consuming + # some unrelated packet, which is undesirable. So you can't even get to + # the handshake stage successfully. + # + # - if we use a memory BIO, then DTLSv1_listen works OK on invalid packets + # -- first the "peek" consumes them, and then it tries to "read" again to + # consume them, which fails immediately, and OpenSSL ignores the failure. + # So it works by accident. BUT, when we get a valid ClientHello, we have + # a problem: DTLSv1_listen tries to "peek" it and then leave it in the + # read BIO for do_handshake to consume. But instead "peek" consumes the + # packet, so it's not there where do_handshake is expecting it, and the + # handshake fails. + # + # Fortunately (if that's the word), we can work around the memory BIO + # problem. (Which is good, because in real life probably all our users will + # be using memory BIOs.) All we have to do is to save the valid ClientHello + # before calling DTLSv1_listen, and then after it returns we push *a second + # copy of it* of the packet memory BIO before calling do_handshake. This + # fakes out OpenSSL and makes it think the "peek" operation worked + # correctly, and we can go on with our lives. + # + # In fact, we push the second copy of the ClientHello unconditionally. On + # new versions of OpenSSL, this is unnecessary, but harmless, because the + # DTLS state machine treats it like a network hiccup that duplicated a + # packet, which DTLS is robust against. + + # Arbitrary number larger than any conceivable handshake volley. + LARGE_BUFFER = 65536 + + def _test_handshake_and_data(self, srtp_profile: bytes | None) -> None: + s_ctx = Context(DTLS_METHOD) + + def generate_cookie(ssl: Connection) -> bytes: + return b"xyzzy" + + def verify_cookie(ssl: Connection, cookie: bytes) -> bool: + return cookie == b"xyzzy" + + s_ctx.set_cookie_generate_callback(generate_cookie) + s_ctx.set_cookie_verify_callback(verify_cookie) + s_ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + s_ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) + s_ctx.set_options(OP_NO_QUERY_MTU) + if srtp_profile is not None: + s_ctx.set_tlsext_use_srtp(srtp_profile) + s = Connection(s_ctx) + s.set_accept_state() + + c_ctx = Context(DTLS_METHOD) + c_ctx.set_options(OP_NO_QUERY_MTU) + if srtp_profile is not None: + c_ctx.set_tlsext_use_srtp(srtp_profile) + c = Connection(c_ctx) + c.set_connect_state() + + # These are mandatory, because openssl can't guess the MTU for a memory + # bio and will produce a mysterious error if you make it try. + c.set_ciphertext_mtu(1500) + s.set_ciphertext_mtu(1500) + + latest_client_hello = None + + def pump_membio( + label: str, source: Connection, sink: Connection + ) -> bool: + try: + chunk = source.bio_read(self.LARGE_BUFFER) + except WantReadError: + return False + # I'm not sure this check is needed, but I'm not sure it's *not* + # needed either: + if not chunk: # pragma: no cover + return False + # Gross hack: if this is a ClientHello, save it so we can find it + # later. See giant comment above. + try: + # if ContentType == handshake and HandshakeType == + # client_hello: + if chunk[0] == 22 and chunk[13] == 1: + nonlocal latest_client_hello + latest_client_hello = chunk + except IndexError: # pragma: no cover + pass + print(f"{label}: {chunk.hex()}") + sink.bio_write(chunk) + return True + + def pump() -> None: + # Raises if there was no data to pump, to avoid infinite loops if + # we aren't making progress. + assert pump_membio("s -> c", s, c) or pump_membio("c -> s", c, s) + + c_handshaking = True + s_listening = True + s_handshaking = False + first = True + while c_handshaking or s_listening or s_handshaking: + if not first: + pump() + first = False + + if c_handshaking: + try: + c.do_handshake() + except WantReadError: + pass + else: + c_handshaking = False + + if s_listening: + try: + s.DTLSv1_listen() + except WantReadError: + pass + else: + s_listening = False + s_handshaking = True + # Write the duplicate ClientHello. See giant comment above. + assert latest_client_hello is not None + s.bio_write(latest_client_hello) + + if s_handshaking: + try: + s.do_handshake() + except WantReadError: + pass + else: + s_handshaking = False + + s.write(b"hello") + pump() + assert c.read(100) == b"hello" + c.write(b"goodbye") + pump() + assert s.read(100) == b"goodbye" + + # Check whether SRTP was negotiated + if srtp_profile is not None: + assert s.get_selected_srtp_profile() == srtp_profile + assert c.get_selected_srtp_profile() == srtp_profile + else: + assert s.get_selected_srtp_profile() == b"" + assert c.get_selected_srtp_profile() == b"" + + # Check that the MTU set/query functions are doing *something* + c.set_ciphertext_mtu(1000) + assert 500 < c.get_cleartext_mtu() < 1000 + c.set_ciphertext_mtu(500) + assert 0 < c.get_cleartext_mtu() < 500 + + @pytest.mark.skipif( + OP_COOKIE_EXCHANGE is None, + reason="DTLS cookie exchange not supported", + ) + def test_it_works_at_all(self) -> None: + self._test_handshake_and_data(srtp_profile=None) + + @pytest.mark.skipif( + OP_COOKIE_EXCHANGE is None, + reason="DTLS cookie exchange not supported", + ) + def test_it_works_with_srtp(self) -> None: + self._test_handshake_and_data(srtp_profile=b"SRTP_AES128_CM_SHA1_80") + + @pytest.mark.skipif( + OP_COOKIE_EXCHANGE is None, + reason="DTLS cookie exchange not supported", + ) + def test_cookie_generate_too_long(self) -> None: + s_ctx = Context(DTLS_METHOD) + + def generate_cookie(ssl: Connection) -> bytes: + return b"\x00" * 256 + + def verify_cookie(ssl: Connection, cookie: bytes) -> bool: + return True + + s_ctx.set_cookie_generate_callback(generate_cookie) + s_ctx.set_cookie_verify_callback(verify_cookie) + s_ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) + s_ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) + s_ctx.set_options(OP_NO_QUERY_MTU) + s = Connection(s_ctx) + s.set_accept_state() + + c_ctx = Context(DTLS_METHOD) + c_ctx.set_options(OP_NO_QUERY_MTU) + c = Connection(c_ctx) + c.set_connect_state() + + c.set_ciphertext_mtu(1500) + s.set_ciphertext_mtu(1500) + + # Client sends ClientHello + try: + c.do_handshake() + except SSL.WantReadError: + pass + chunk = c.bio_read(self.LARGE_BUFFER) + s.bio_write(chunk) + + # Server tries DTLSv1_listen, which triggers cookie generation. + # The oversized cookie should raise ValueError. + with pytest.raises(ValueError, match="Cookie too long"): + s.DTLSv1_listen() + + def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: + c_ctx = Context(DTLS_METHOD) + c = Connection(c_ctx) + + # No timeout before the handshake starts. + assert c.DTLSv1_get_timeout() is None + assert c.DTLSv1_handle_timeout() is False + + # Start handshake and check there is data to send. + c.set_connect_state() + try: + c.do_handshake() + except SSL.WantReadError: + pass + assert c.bio_read(self.LARGE_BUFFER) + + # There should now be an active timeout. + seconds = c.DTLSv1_get_timeout() + assert seconds is not None + + # Handle the timeout and check there is data to send. + time.sleep(seconds) + assert c.DTLSv1_handle_timeout() is True + assert c.bio_read(self.LARGE_BUFFER) + + # After the maximum number of allowed timeouts is reached, + # DTLSv1_handle_timeout will return -1. + # + # Testing this directly is prohibitively time consuming as the timeout + # duration is doubled on each retry, so the best we can do is to mock + # this condition. + monkeypatch.setattr(_lib, "DTLSv1_handle_timeout", lambda x: -1) + + with pytest.raises(Error): + c.DTLSv1_handle_timeout() diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 000000000..cf81330f7 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,19 @@ +import pytest + +from OpenSSL._util import exception_from_error_queue, lib + + +class TestErrors: + """ + Tests for handling of certain OpenSSL error cases. + """ + + def test_exception_from_error_queue_nonexistent_reason(self) -> None: + """ + :func:`exception_from_error_queue` raises ``ValueError`` when it + encounters an OpenSSL error code which does not have a reason string. + """ + lib.ERR_put_error(lib.ERR_LIB_EVP, 0, 1112, b"", 10) + with pytest.raises(ValueError) as exc: + exception_from_error_queue(ValueError) + assert exc.value.args[0][0][2] in ("", "unknown error") diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 000000000..d5505580a --- /dev/null +++ b/tests/util.py @@ -0,0 +1,13 @@ +# Copyright (C) Jean-Paul Calderone +# Copyright (C) Twisted Matrix Laboratories. +# See LICENSE for details. +""" +Helpers for the OpenSSL test suite, largely copied from +U{Twisted}. +""" + +# This is the UTF-8 encoding of the SNOWMAN unicode code point. +NON_ASCII = b"\xe2\x98\x83".decode("utf-8") + +# The type name expected in warnings about using the wrong string type. +WARNING_TYPE_EXPECTED = "str" diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 67386d1a8..000000000 --- a/tox.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = pypy,py26,py27,py32,py33 - -[testenv] -setenv = - # Do not allowed the executing environment to pollute the test environment - # with extra packages. - PYTHONPATH= -# The standard library unittest module can run tests on Python 2.7 and newer -commands = python setup.py test