service_identity-24.2.0/.git_archival.txt0000644000000000000000000000014614707112756015364 0ustar00node: $Format:%H$ node-date: $Format:%cI$ describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ service_identity-24.2.0/.gitattributes0000644000000000000000000000021714707112756015003 0ustar00# Force LF line endings for text files * text=auto eol=lf # Needed for hatch-vcs / setuptools-scm-git-archive .git_archival.txt export-subst service_identity-24.2.0/.pre-commit-config.yaml0000644000000000000000000000116314707112756016372 0ustar00--- repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.7.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell args: [-L, fo] - repo: https://github.com/econchick/interrogate rev: 1.7.0 hooks: - id: interrogate args: [tests] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-toml - id: check-yaml service_identity-24.2.0/.python-version-default0000644000000000000000000000000514707112756016532 0ustar003.13 service_identity-24.2.0/.readthedocs.yaml0000644000000000000000000000051314707112756015336 0ustar00--- version: 2 build: os: ubuntu-lts-latest tools: # Keep in-sync with tox.ini/docs and ci.yml/docs python: "3.12" jobs: # Need the tags to calculate the version (sometimes). post_checkout: - git fetch --tags python: install: - method: pip path: . extra_requirements: - docs service_identity-24.2.0/CHANGELOG.md0000644000000000000000000001712514707112756013727 0ustar00# Changelog All notable changes to this project will be documented in this file. The format is based on [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/) and this project adheres to [*Calendar Versioning*](https://calver.org/). The **first number** of the version is the year. The **second number** is incremented with each release, starting at 1 for each year. The **third number** is for emergencies when we need to start branches for older releases. You can find out backwards-compatibility policy [here](https://github.com/pyca/service-identity/blob/main/.github/SECURITY.md). ## [24.2.0](https://github.com/pyca/service-identity/compare/24.1.0...24.2.0) - 2024-10-26 ### Added - Python 3.13 is now officially supported. [#74](https://github.com/pyca/service-identity/pull/74) ### Changed - pyOpenSSL's identity extraction has been reimplemented using *cryptography*'s primitives instead of deprecated pyOpenSSL APIs. As a result, the oldest supported pyOpenSSL version is now 17.1.0. [#70](https://github.com/pyca/service-identity/pull/70) ## [24.1.0](https://github.com/pyca/service-identity/compare/23.1.0...24.1.0) - 2024-01-14 ### Changed - If a certificate doesn't contain any `subjectAltName`s, we now raise `service_identity.CertificateError` instead of `service_identity.VerificationError` to make the problem easier to debug. [#67](https://github.com/pyca/service-identity/pull/67) ## [23.1.0](https://github.com/pyca/service-identity/compare/21.1.0...23.1.0) - 2023-06-14 ### Removed - All Python versions up to and including 3.7 have been dropped. - Support for `commonName` in certificates has been dropped. It has been deprecated since 2017 and isn't supported by any major browser. - The oldest supported pyOpenSSL version (when using the `pyopenssl` backend) is now 17.0.0. When using such an old pyOpenSSL version, you have to pin *cryptography* yourself to ensure compatibility between them. Please check out [`constraints/oldest-pyopenssl.txt`](https://github.com/pyca/service-identity/blob/main/tests/constraints/oldest-pyopenssl.txt) to verify what we are testing against. ### Deprecated - If you've used `service_identity.(cryptography|pyopenssl).extract_ids()`, please switch to the new names `extract_patterns()`. [#56](https://github.com/pyca/service-identity/pull/56) ### Added - `service_identity.(cryptography|pyopenssl).extract_patterns()` are now public APIs (FKA `extract_ids()`). You can use them to extract the patterns from a certificate without verifying anything. [#55](https://github.com/pyca/service-identity/pull/55) - *service-identity* is now fully typed. [#57](https://github.com/pyca/service-identity/pull/57) ## [21.1.0](https://github.com/pyca/service-identity/compare/18.1.0...21.1.0) - 2021-05-09 ### Removed - Python 3.4 is not supported anymore. It has been unsupported by the Python core team for a while now, its PyPI downloads are negligible, and our CI provider removed it as a supported option. It's very unlikely that `service-identity` will break under 3.4 anytime soon, which is why we do *not* block its installation on Python 3.4. But we don't test it anymore and will block it once someone reports breakage. ### Fixed - `service_identity.exceptions.VerificationError` can now be pickled and is overall more well-behaved as an exception. This raises the requirement of `attrs` to 19.1.0. ## [18.1.0](https://github.com/pyca/service-identity/compare/17.0.0...18.1.0) - 2018-12-05 ### Added - pyOpenSSL is optional now if you use `service_identity.cryptography.*` only. - Added support for `iPAddress` `subjectAltName`s. You can now verify whether a connection or a certificate is valid for an IP address using `service_identity.pyopenssl.verify_ip_address()` and `service_identity.cryptography.verify_certificate_ip_address()`. [#12](https://github.com/pyca/service-identity/pull/12) ## [17.0.0](https://github.com/pyca/service-identity/compare/16.0.0...17.0.0) - 2017-05-23 ### Deprecated - Since Chrome 58 and Firefox 48 both don't accept certificates that contain only a Common Name, its usage is hereby deprecated in `service-identity` too. We have been raising a warning since 16.0.0 and the support will be removed in mid-2018 for good. ### Added - When `service_identity.SubjectAltNameWarning` is raised, the Common Name of the certificate is now included in the warning message. [#17](https://github.com/pyca/service-identity/pull/17) - Added `cryptography.x509` backend for verifying certificates. [#18](https://github.com/pyca/service-identity/pull/18) ### Changed - Wildcards (`*`) are now only allowed if they are the leftmost label in a certificate. This is common practice by all major browsers. [#19](https://github.com/pyca/service-identity/pull/19) ## [16.0.0](https://github.com/pyca/service-identity/compare/14.0.0...16.0.0) - 2016-02-18 ### Removed - Python 3.3 and 2.6 aren't supported anymore. They may work by chance but any effort to keep them working has ceased. The last Python 2.6 release was on October 29, 2013 and isn't supported by the CPython core team anymore. Major Python packages like Django and Twisted dropped Python 2.6 a while ago already. Python 3.3 never had a significant user base and wasn't part of any distribution's LTS release. - pyOpenSSL versions older than 0.14 are not tested anymore. They don't even build on recent OpenSSL versions. Please note that its support may break without further notice. ### Added - Officially support Python 3.5. - A `__str__` method to `VerificationError`. ### Changed - `service_identity.SubjectAltNameWarning` is now raised if the server certificate lacks a proper `SubjectAltName`. [#9](https://github.com/pyca/service-identity/issues/9) - Port from `characteristic` to its spiritual successor [attrs](https://www.attrs.org/). ## [14.0.0](https://github.com/pyca/service-identity/compare/1.0.0...14.0.0) - 2014-08-22 ### Changed - Switch to year-based version numbers. - Port to `characteristic` 14.0 (get rid of deprecation warnings). - Package docs with source distributions. ## [1.0.0](https://github.com/pyca/service-identity/compare/0.2.0...1.0.0) - 2014-06-15 ### Removed - Drop support for Python 3.2. There is no justification to add complexity and unnecessary function calls for a Python version that [nobody uses](https://alexgaynor.net/2014/jan/03/pypi-download-statistics/). ### Changed - Move into the [Python Cryptography Authority’s GitHub account](https://github.com/pyca/). - Move exceptions into `service_identity.exceptions` so tracebacks don’t contain private module names. - Promoting to stable since Twisted 14.0 is optionally depending on `service-identity` now. - Use [characteristic](https://characteristic.readthedocs.io/) instead of a home-grown solution. ### Fixed - `idna` 0.6 did some backward-incompatible fixes that broke Python 3 support. This has been fixed now therefore *service-identity* only works with `idna` 0.6 and later. Unfortunately since `idna` doesn’t offer version introspection, *service-identity* can’t warn about it. ## [0.2.0](https://github.com/pyca/service-identity/compare/0.1.0...0.2.0) - 2014-04-06 ### Added - Official support for Python 3.4. ### Changed - Refactor into a multi-module package. Most notably, `verify_hostname` and `extract_ids` live in the `service_identity.pyopenssl` module now. - `verify_hostname` now takes an `OpenSSL.SSL.Connection` for the first argument. - More strict checks for URI_IDs. ### Fixed - Less false positives in IP address detection. ## [0.1.0](https://github.com/pyca/service-identity/tree/0.1.0) - 2014-03-03 Initial release. service_identity-24.2.0/README.md0000644000000000000000000000604514707112756013374 0ustar00# Service Identity Verification Documentation License: MIT PyPI release Downloads per month PyCA on IRC Use this package if: - you want to **verify** that a [PyCA *cryptography*](https://cryptography.io/) certificate is valid for a certain hostname or IP address, - or if you use [pyOpenSSL](https://pypi.org/project/pyOpenSSL/) and don’t want to be [**MITM**](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)ed, - or if you want to **inspect** certificates from either for service IDs. *service-identity* aspires to give you all the tools you need for verifying whether a certificate is valid for the intended purposes. In the simplest case, this means *host name verification*. However, *service-identity* implements [RFC 6125](https://datatracker.ietf.org/doc/html/rfc6125.html) fully. Also check out [*pem*](https://github.com/hynek/pem) that makes loading certificates from all kinds of PEM-encoded files a breeze! ## Project Information *service-identity* is released under the [MIT](https://github.com/pyca/service-identity/blob/main/LICENSE) license, its documentation lives at [Read the Docs](https://service-identity.readthedocs.io/), the code on [GitHub](https://github.com/pyca/service-identity), and the latest release on [PyPI](https://pypi.org/project/service-identity/). ### Credits *service-identity* is written and maintained by [Hynek Schlawack](https://hynek.me/). The development is kindly supported by my employer [Variomedia AG](https://www.variomedia.de/), *service-identity*'s [Tidelift subscribers](https://tidelift.com/lifter/search/pypi/service-identity), and all my amazing [GitHub Sponsors](https://github.com/sponsors/hynek). ### *service-identity* for Enterprise Available as part of the [Tidelift Subscription](https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek). The maintainers of *service-identity* and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open-source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. service_identity-24.2.0/tox.ini0000644000000000000000000000346114707112756013427 0ustar00[tox] min_version = 4 env_list = lint, mypy-{api,pkg}, docs, py3{8,9,10,11,12,13}{,-pyopenssl}{,-oldest}{,-idna}, coverage-report [testenv] package = wheel wheel_build_env = .pkg extras = tests idna: idna deps = pyopenssl: pyopenssl pass_env = FORCE_COLOR NO_COLOR set_env = oldest: PIP_CONSTRAINT = tests/constraints/oldest-cryptography.txt pyopenssl-oldest: PIP_CONSTRAINT = tests/constraints/oldest-pyopenssl.txt commands = coverage run -m pytest {posargs} py312-pyopenssl-latest-idna: coverage run -m pytest --doctest-modules --doctest-glob='*.rst' {posargs} [testenv:coverage-report] # keep in-sync with .python-version-default base_python = py312 deps = coverage[toml]>=5.0.2 skip_install = true commands = coverage combine coverage report [testenv:lint] skip_install = true deps = pre-commit-uv commands = pre-commit run --all-files {posargs} [testenv:mypy-api] extras = mypy commands = mypy tests/typing docs/pyopenssl_example.py [testenv:mypy-pkg] extras = mypy commands = mypy src [testenv:docs] # Keep in-sync with gh-actions and .readthedocs.yaml. base_python = py312 extras = docs commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html [testenv:docs-watch] package = editable base_python = {[testenv:docs]base_python} extras = {[testenv:docs]extras} deps = watchfiles commands = watchfiles \ --ignore-paths docs/_build/ \ 'sphinx-build -W -n --jobs auto -b html -d {envtmpdir}/doctrees docs docs/_build/html' \ src \ docs [testenv:docs-linkcheck] base_python = {[testenv:docs]base_python} extras = {[testenv:docs]extras} commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/html service_identity-24.2.0/.github/CODE_OF_CONDUCT.md0000644000000000000000000000613714707112756016256 0ustar00# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make 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 the project team at . 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](https://www.contributor-covenant.org), version 1.4, available at \<>. service_identity-24.2.0/.github/CONTRIBUTING.md0000644000000000000000000002122414707112756015702 0ustar00# How To Contribute Thank you for considering contributing to *service-identity*! It's people like *you* who make it such a great tool for everyone. This document intends to make contribution more accessible by codifying tribal knowledge and expectations. Don't be afraid to open half-finished PRs, and ask questions if something is unclear! Please note that this project is released with a Contributor [Code of Conduct](https://github.com/pyca/service-identity/blob/main/.github/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. Please report any harm to [Hynek Schlawack] in any way you find appropriate. ## Workflow - No contribution is too small! Please submit as many fixes for typos and grammar bloopers as you can! - Try to limit each pull request to *one* change only. - Since we squash on merge, it's up to you how you handle updates to the `main` branch. Whether you prefer to rebase on `main` or merge `main` into your branch, do whatever is more comfortable for you. - *Always* add tests and docs for your code. This is a hard rule; patches with missing tests or documentation won't be merged. - Make sure your changes pass our [CI]. You won't get any feedback until it's green unless you ask for it. For the CI to pass, the coverage must be 100%. If you have problems to test something, open anyway and ask for advice. In some situations, we may agree to add an `# pragma: no cover`. - Once you've addressed review feedback, make sure to bump the pull request with a short note, so we know you're done. - Don’t break backwards-compatibility. ## Local Development Environment You can (and should) run our test suite using [*tox*]. However, you’ll probably want a more traditional environment as well. First, create a virtual environment so you don't break your system-wide Python installation. We recommend using the Python version from the `.python-version-default` file in project's root directory. If you're using [*direnv*](https://direnv.net), you can automate the creation of a virtual environment with the correct Python version by adding the following `.envrc` to the project root after cloning the repository: ```bash layout python python$(cat .python-version-default) ``` If you're using tools that understand `.python-version` files like [*pyenv*](https://github.com/pyenv/pyenv) does, you can make it a link to the `.python-version-default` file. --- [Create a fork](https://github.com/pyca/service-identity/fork) of the *service-identity* repository and clone it: ```console $ git clone git@github.com:YOU/service-identity.git ``` Or if you prefer to use Git via HTTPS: ```console $ git clone https://github.com/YOU/service-identity.git ``` > **Warning** > - **Before** you start working on a new pull request, use the "*Sync fork*" button in GitHub's web UI to ensure your fork is up to date. > - **Always create a new branch off `main` for each new pull request.** > Yes, you can work on `main` in your fork and submit pull requests. > But this will *inevitably* lead to you not being able to synchronize your fork with upstream and having to start over. Change into the newly created directory and after activating a virtual environment, install an editable version of *service-identity* along with its tests requirements: ```console $ cd service-identity $ python -Im pip install --upgrade pip wheel # PLEASE don't skip this step $ python -Im pip install -e '.[dev]' ``` At this point, ```console $ python -Im pytest ``` When working on the documentation, use: ```console $ tox run -e docs-watch ``` ... to watch your files and automatically rebuild when a file changes. And use: ```console $ tox run -e docs ``` ... to build it once and run our doctests. The built documentation can then be found in `docs/_build/html/`. --- To avoid committing code that violates our style guide, we strongly advise you to install [*pre-commit*] and its hooks: ```console $ pre-commit install ``` This is not strictly necessary, because our [*tox*] file contains an environment that runs: ```console $ pre-commit run --all-files ``` But it's way more comfortable to run it locally and catch avoidable errors before pushing them to GitHub. ## Code - Obey [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/). We use the `"""`-on-separate-lines style for docstrings with [Napoleon](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html)-style API documentation: ```python def func(x: str, y: int) -> str: """ Do something. Args: x: A very important parameter. y: Another important parameter but one that doesn't fit a line so it already starts on the next one. Raises: Exception: When Something goes wrong. Returns: A very important return value. """ ``` - If you add or change public APIs, tag the docstring using `.. versionadded:: 16.0.0 WHAT` or `.. versionchanged:: 16.2.0 WHAT`. - We follow [PEP 8](https://peps.python.org/pep-0008/) as enforced by [Ruff](https://ruff.rs/) with a line length of 79 characters. As long as you run our full [*tox*] suite before committing, or install our [*pre-commit*] hooks (ideally you'll do both – see [*Local Development Environment*](#local-development-environment) above), you won't have to spend any time on formatting your code at all. If you don't, [CI] will catch it for you – but that seems like a waste of your time! ## Tests - Write your asserts as `expected == actual` to line them up nicely: ```python x = f() assert 42 == x.some_attribute assert "foo" == x._a_private_attribute ``` - To run the test suite, all you need is a recent [*tox*]. It will ensure the test suite runs with all dependencies against all Python versions just as it will in our [CI]. - Write [good test docstrings](https://jml.io/pages/test-docstrings.html). - If you've changed or added public APIs, please update our type stubs (files ending in `.pyi`). ## Documentation - We use [Markdown] everywhere except in `docs/api.rst` and docstrings. - Use [semantic newlines] in [*reStructuredText*] and [Markdown] files (files ending in `.rst` and `.md`): ```markdown This is a sentence. This is another sentence. ``` - If you start a new section, add two blank lines before and one blank line after the header, except if two headers follow immediately after each other: ```markdown Last line of previous section. ## Header of New Top Section ### Header of New Section First line of new section. ``` ### Changelog If your change is noteworthy, there needs to be a changelog entry in [`CHANGELOG.md`](https://github.com/pyca/service-identity/blob/main/CHANGELOG.md), so our users can learn about it! - The changelog follows the [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/) standard. Please add the best-fitting section if it's missing for the current release. We use the following order: `Security`, `Removed`, `Deprecated`, `Added`, `Changed`, `Fixed`. - As with other docs, please use [semantic newlines] in the changelog. - Make the last line a link to your pull request. You probably have to open it first to know the number. - Wrap symbols like modules, functions, or classes into backticks so they are rendered in a `monospace font`. - Wrap arguments into asterisks like in docstrings: `Added new argument *an_argument*.` - If you mention functions or other callables, add parentheses at the end of their names: `service-identity.func()` or `service-identity.Class.method()`. This makes the changelog a lot more readable. - Prefer simple past tense or constructions with "now". For example: * Added `service-identity.func()`. * `service-identity.func()` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. #### Example entries ```markdown - Added `service-identity.func()`. The feature really *is* awesome. [#1](https://github.com/pyca/service-identity/pull/1) ``` or: ```markdown - `service-identity.func()` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. The bug really *was* nasty. [#1](https://github.com/pyca/service-identity/pull/1) ``` [CI]: https://github.com/pyca/service-identity/actions [Hynek Schlawack]: https://hynek.me/about/ [*pre-commit*]: https://pre-commit.com/ [*tox*]: https://tox.wiki/ [semantic newlines]: https://rhodesmill.org/brandon/2012/one-sentence-per-line/ [*reStructuredText*]: https://www.sphinx-doc.org/en/stable/usage/restructuredtext/basics.html [Markdown]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax service_identity-24.2.0/.github/FUNDING.yml0000644000000000000000000000006414707112756015265 0ustar00--- github: hynek tidelift: "pypi/service-identity" service_identity-24.2.0/.github/SECURITY.md0000644000000000000000000000140514707112756015241 0ustar00# Security Policy ## Supported Versions We are following [Calendar Versioning](https://calver.org) with generous backwards-compatibility guarantees. *Therefore we only support the latest version*. That said, you shouldn't be afraid to upgrade if you're only using our documented public APIs and pay attention to `DeprecationWarning`s. Whenever there is a need to break compatibility, it is announced [in the changelog](https://github.com/pyca/service-identity/blob/main/CHANGELOG.md) and raises a `DeprecationWarning` for a year (if possible) before it's finally really broken. ## Reporting a Vulnerability To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. service_identity-24.2.0/.github/dependabot.yml0000644000000000000000000000017314707112756016301 0ustar00--- version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" service_identity-24.2.0/.github/workflows/ci.yml0000644000000000000000000001257314707112756016633 0ustar00--- name: CI on: push: branches: [main] tags: ["*"] pull_request: workflow_dispatch: env: FORCE_COLOR: "1" # Make tools pretty. PIP_DISABLE_PIP_VERSION_CHECK: "1" PIP_NO_PYTHON_VERSION_WARNING: "1" permissions: {} jobs: lint: name: Run linters runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version-file: .python-version-default - uses: hynek/setup-cached-uv@v2 - run: > uvx --with tox-uv tox run -e lint -- --show-diff-on-failure build-package: name: Build & verify package runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: hynek/build-and-inspect-python-package@v2 id: baipp outputs: # Used to define the matrix for tests below. The value is based on # packaging metadata (trove classifiers). python-versions: ${{ steps.baipp.outputs.supported_python_classifiers_json_array }} tests: name: Tests & Mypy API on ${{ matrix.python-version }} runs-on: ubuntu-latest needs: build-package strategy: matrix: # Created by the build-and-inspect-python-package action above. python-version: ${{ fromJson(needs.build-package.outputs.python-versions) }} steps: - name: Download pre-built packages uses: actions/download-artifact@v4 with: name: Packages path: dist - run: | tar xf dist/*.tar.gz --strip-components=1 rm -rf src - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - uses: hynek/setup-cached-uv@v2 - name: Run tests run: > uvx --with tox-uv tox run --installpkg dist/*.whl -f py$(echo ${{ matrix.python-version }} | tr -d .) - name: Upload coverage data uses: actions/upload-artifact@v4 with: name: coverage-data-${{ matrix.python-version }} path: .coverage.* include-hidden-files: true if-no-files-found: ignore - name: Check public API with Mypy run: > uvx --with tox-uv tox run --installpkg dist/*.whl -e mypy-api coverage: name: Ensure 100% test coverage needs: tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version-file: .python-version-default - uses: hynek/setup-cached-uv@v2 - uses: actions/download-artifact@v4 with: pattern: coverage-data-* merge-multiple: true - name: Combine coverage & fail if it's <100% run: | uv tool install coverage[toml] coverage combine coverage html --skip-covered --skip-empty # Report and write to summary. coverage report --format=markdown >> $GITHUB_STEP_SUMMARY # Report again and fail if under 100%. coverage report --fail-under=100 - name: Upload HTML report if check failed. uses: actions/upload-artifact@v4 with: name: html-report path: htmlcov if: ${{ failure() }} mypy-pkg: name: Mypy Codebase runs-on: ubuntu-latest needs: build-package steps: - name: Download pre-built packages uses: actions/download-artifact@v4 with: name: Packages path: dist - run: tar xf dist/*.tar.gz --strip-components=1 - uses: actions/setup-python@v5 with: python-version-file: .python-version-default - uses: hynek/setup-cached-uv@v2 - run: > uvx --with tox-uv tox run -e mypy-pkg install-dev: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] name: Verify dev env runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version-file: .python-version-default cache: pip - name: Install in dev mode & import run: | python -Im pip install -e .[dev] python -Ic 'import service_identity; print(service_identity.__version__)' python -Ic 'import service_identity.pyopenssl' python -Ic 'import service_identity.cryptography' docs: name: Build docs & run doctests needs: build-package runs-on: ubuntu-latest steps: - name: Download pre-built packages uses: actions/download-artifact@v4 with: name: Packages path: dist - run: tar xf dist/*.tar.gz --strip-components=1 - uses: actions/setup-python@v5 with: # Keep in sync with tox.ini/docs & .readthedocs.yaml python-version: "3.12" - uses: hynek/setup-cached-uv@v2 - run: > uvx --with tox-uv tox run -e docs required-checks-pass: name: Ensure everything required is passing for branch protection if: always() needs: - coverage - docs - install-dev - lint - mypy-pkg runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} service_identity-24.2.0/.github/workflows/codeql-analysis.yml0000644000000000000000000000136514707112756021325 0ustar00--- name: CodeQL on: push: branches: [main] pull_request: branches: [main] schedule: - cron: "41 3 * * 6" permissions: contents: read jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [python] steps: - name: Checkout repository uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 service_identity-24.2.0/.github/workflows/pypi-package.yml0000644000000000000000000000323714707112756020607 0ustar00--- name: Build & maybe upload PyPI package on: push: branches: [main] tags: ["*"] release: types: - published workflow_dispatch: permissions: attestations: write contents: read id-token: write jobs: # Always build & lint package. build-package: name: Build & verify package runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: hynek/build-and-inspect-python-package@v2 with: attest-build-provenance-github: 'true' # Upload to Test PyPI on every commit on main. release-test-pypi: name: Publish in-dev package to test.pypi.org environment: release-test-pypi if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest needs: build-package steps: - name: Download packages built by build-and-inspect-python-package uses: actions/download-artifact@v4 with: name: Packages path: dist - name: Upload package to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ # Upload to real PyPI on GitHub Releases. release-pypi: name: Publish released package to pypi.org environment: release-pypi if: github.event.action == 'published' runs-on: ubuntu-latest needs: build-package steps: - name: Download packages built by build-and-inspect-python-package uses: actions/download-artifact@v4 with: name: Packages path: dist - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 service_identity-24.2.0/docs/Makefile0000644000000000000000000001522214707112756014502 0ustar00# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/service_identity.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/service_identity.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/service_identity" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/service_identity" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." service_identity-24.2.0/docs/api.rst0000644000000000000000000000347614707112756014355 0ustar00=== API === .. note:: So far, public high-level APIs are only available for host names (:rfc:`6125`) and IP addresses (:rfc:`2818`). All IDs specified by :rfc:`6125` are already implemented though. If you'd like to play with them and provide feedback have a look at the ``verify_service_identity`` function in the `hazmat module `_. PyCA cryptography ================= .. currentmodule:: service_identity.cryptography .. autofunction:: verify_certificate_hostname .. autofunction:: verify_certificate_ip_address .. autofunction:: extract_patterns pyOpenSSL ========= .. currentmodule:: service_identity.pyopenssl .. autofunction:: verify_hostname In practice, this may look like the following: .. include:: pyopenssl_example.py :literal: .. autofunction:: verify_ip_address .. autofunction:: extract_patterns Hazardous Materials =================== .. currentmodule:: service_identity.hazmat .. danger:: The following APIs require reader's discretion. They are stable and they've been using internally by *service-identity* for years, but you need to know what you're doing. Pattern Objects --------------- The following are the objects return by the ``extract_patterns`` functions. They each carry the attributes that are necessary to match an ID of their type. .. autoclass:: CertificatePattern It includes all of those that follow now. .. autoclass:: DNSPattern :members: .. autoclass:: IPAddressPattern :members: .. autoclass:: URIPattern :members: .. autoclass:: SRVPattern :members: Universal Errors and Warnings ============================= .. currentmodule:: service_identity .. autoexception:: VerificationError .. autoexception:: CertificateError .. autoexception:: SubjectAltNameWarning service_identity-24.2.0/docs/changelog.md0000644000000000000000000000004114707112756015304 0ustar00```{include} ../CHANGELOG.md ``` service_identity-24.2.0/docs/conf.py0000644000000000000000000000704214707112756014342 0ustar00from importlib import metadata # 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.doctest", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "myst_parser", "notfound.extension", ] myst_enable_extensions = [ "colon_fence", "smartquotes", "deflist", ] # Move type hints into the description block, instead of the func definition. autodoc_typehints = "description" autodoc_typehints_description_target = "documented" # GitHub has rate limits linkcheck_ignore = [ r"https://github.com/.*/(issues|pull|compare)/\d+", r"https://twitter.com/.*", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = "service-identity" year = 2014 copyright = "2014, Hynek Schlawack" # 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. release = metadata.version("service-identity") # The short X.Y version. version = release.rsplit(".", 1)[0] # Avoid confusing in-dev versions. if "dev" in release: release = version = "UNRELEASED" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # -- Options for HTML output ---------------------------------------------- html_theme = "furo" html_theme_options = { "top_of_page_buttons": [], "light_css_variables": { "font-stack": "Inter, sans-serif", "font-stack--monospace": "BerkeleyMono, MonoLisa, ui-monospace, " "SFMono-Regular, Menlo, Consolas, Liberation Mono, monospace", }, } html_static_path = ["_static"] html_css_files = ["custom.css"] # Output file base name for HTML help builder. htmlhelp_basename = "service-identitydoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = {} # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( "index", "service-identity.tex", "service\\_identity Documentation", "Hynek Schlawack", "manual", ) ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "service-identity", "service-identity Documentation", ["Hynek Schlawack"], 1, ) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "service-identity", "service-identity Documentation", "Hynek Schlawack", "service-identity", "Service Identity Verification for pyOpenSSL", "Miscellaneous", ) ] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "pyopenssl": ("https://www.pyopenssl.org/en/stable/", None), "cryptography": ("https://cryptography.io/en/stable/", None), } service_identity-24.2.0/docs/implemented-standards.md0000644000000000000000000000026514707112756017651 0ustar00# Implemented Standards - `dNSName` (DNS-ID, aka host names, {rfc}`6125`). - `iPAddress` ({rfc}`2818`). - `uniformResourceIdentifier` (URI-ID, {rfc}`6125`). - SRV-ID ({rfc}`6125`) service_identity-24.2.0/docs/index.md0000644000000000000000000000155214707112756014474 0ustar00# Service Identity Verification for pyOpenSSL & cryptography Release **{sub-ref}`release`** ({doc}`What's new? `) ```{include} ../README.md :start-after: "spiel-begin -->" :end-before: "## Project Information" ``` ## User's Guide ```{toctree} :maxdepth: 1 installation implemented-standards api ``` ## Indices and tables - {ref}`genindex` - {ref}`search` ## *service-identity* for Enterprise ```{include} ../README.md :start-after: "*service-identity* for Enterprise" ``` ```{toctree} :hidden: :caption: Meta license changelog PyPI GitHub Contributing Security Policy Funding ``` service_identity-24.2.0/docs/installation.md0000644000000000000000000000251114707112756016062 0ustar00# Installation and Requirements ## Installation ```console $ python -Im pip install service-identity ``` ## Requirements *service-identity* depends on the [cryptography] package. In addition to the latest release, we're also testing against the following oldest version constraint: ```{include} ../tests/constraints/oldest-cryptography.txt :literal: true ``` If you want to use the [pyOpenSSL] functionality, you have to install it yourself. In addition to the latest release, we are also testing against the following oldest version constraints (you have to add the *cryptography* pin yourself, if you want to use an old version of pyOpenSSL): ```{include} ../tests/constraints/oldest-pyopenssl.txt :literal: true ``` ### International Domain Names Optionally, the `idna` extra dependency can be used for [internationalized domain names] (IDN), i.e. non-ASCII domains: ```console $ python -Im pip install service-identity[idna] ``` Unfortunately it's required because Python's IDN support in the standard library is [outdated] even in the latest releases. [cryptography]: https://cryptography.io/ [idna]: https://pypi.org/project/idna/ [internationalized domain names]: https://en.wikipedia.org/wiki/Internationalized_domain_name [outdated]: https://github.com/python/cpython/issues/61507 [pyopenssl]: https://pypi.org/project/pyOpenSSL/ service_identity-24.2.0/docs/license.md0000644000000000000000000000042714707112756015007 0ustar00# License and Credits *service-identity* is released under the [MIT](https://github.com/pyca/service-identity/blob/main/LICENSE) license. ```{include} ../README.md :parser: myst_parser.sphinx_ :start-after: "## Credits" :end-before: "### *service-identity* for Enterprise" ``` service_identity-24.2.0/docs/pyopenssl_example.py0000644000000000000000000000224014707112756017157 0ustar00import argparse import pprint import socket import idna from OpenSSL import SSL import service_identity parser = argparse.ArgumentParser( description="Connect to HOST, inspect its certificate " "and verify if it's valid for its hostname." ) parser.add_argument("HOST") args = parser.parse_args() hostname = args.HOST ctx = SSL.Context(SSL.TLSv1_2_METHOD) ctx.set_verify(SSL.VERIFY_PEER, lambda conn, cert, errno, depth, ok: bool(ok)) ctx.set_default_verify_paths() conn = SSL.Connection(ctx, socket.socket(socket.AF_INET, socket.SOCK_STREAM)) conn.set_tlsext_host_name(idna.encode(hostname)) conn.connect((hostname, 443)) try: conn.do_handshake() if cert := conn.get_peer_certificate(): print("Server certificate is valid for the following patterns:\n") pprint.pprint(service_identity.pyopenssl.extract_patterns(cert)) try: service_identity.pyopenssl.verify_hostname(conn, hostname) except service_identity.VerificationError: print(f"\nPresented certificate is NOT valid for {hostname}.") finally: conn.shutdown() except SSL.Error as e: print(f"TLS Handshake failed: {e!r}.") finally: conn.close() service_identity-24.2.0/docs/_static/custom.css0000644000000000000000000000042514707112756016513 0ustar00@import url('https://rsms.me/inter/inter.css'); @import url('https://assets.hynek.me/css/bm.css'); :root { font-feature-settings: 'liga' 1, 'calt' 1; /* fix for Chrome */ } @supports (font-variation-settings: normal) { :root { font-family: InterVariable, sans-serif; } } service_identity-24.2.0/src/service_identity/__init__.py0000644000000000000000000000130414707112756020357 0ustar00""" Verify service identities. """ from . import cryptography, hazmat, pyopenssl from .exceptions import ( CertificateError, SubjectAltNameWarning, VerificationError, ) __title__ = "service-identity" __author__ = "Hynek Schlawack" __license__ = "MIT" __copyright__ = "Copyright (c) 2014 " + __author__ __all__ = [ "CertificateError", "SubjectAltNameWarning", "VerificationError", "hazmat", "cryptography", "pyopenssl", ] def __getattr__(name: str) -> str: if name != "__version__": msg = f"module {__name__} has no attribute {name}" raise AttributeError(msg) from importlib.metadata import version return version("service-identity") service_identity-24.2.0/src/service_identity/cryptography.py0000644000000000000000000001223514707112756021360 0ustar00""" `cryptography.x509 `_-specific code. """ from __future__ import annotations import warnings from typing import Sequence from cryptography.x509 import ( Certificate, DNSName, ExtensionOID, IPAddress, ObjectIdentifier, OtherName, UniformResourceIdentifier, ) from cryptography.x509.extensions import ExtensionNotFound from pyasn1.codec.der.decoder import decode from pyasn1.type.char import IA5String from .exceptions import CertificateError from .hazmat import ( DNS_ID, CertificatePattern, DNSPattern, IPAddress_ID, IPAddressPattern, SRVPattern, URIPattern, verify_service_identity, ) __all__ = ["verify_certificate_hostname"] def verify_certificate_hostname( certificate: Certificate, hostname: str ) -> None: r""" Verify whether *certificate* is valid for *hostname*. .. note:: Nothing is verified about the *authority* of the certificate; the caller must verify that the certificate chains to an appropriate trust root themselves. Args: certificate: A *cryptography* X509 certificate object. hostname: The hostname that *certificate* should be valid for. Raises: service_identity.VerificationError: If *certificate* is not valid for *hostname*. service_identity.CertificateError: If *certificate* contains invalid / unexpected data. This includes the case where the certificate contains no `subjectAltName`\ s. .. versionchanged:: 24.1.0 :exc:`~service_identity.CertificateError` is raised if the certificate contains no ``subjectAltName``\ s instead of :exc:`~service_identity.VerificationError`. """ verify_service_identity( cert_patterns=extract_patterns(certificate), obligatory_ids=[DNS_ID(hostname)], optional_ids=[], ) def verify_certificate_ip_address( certificate: Certificate, ip_address: str ) -> None: r""" Verify whether *certificate* is valid for *ip_address*. .. note:: Nothing is verified about the *authority* of the certificate; the caller must verify that the certificate chains to an appropriate trust root themselves. Args: certificate: A *cryptography* X509 certificate object. ip_address: The IP address that *connection* should be valid for. Can be an IPv4 or IPv6 address. Raises: service_identity.VerificationError: If *certificate* is not valid for *ip_address*. service_identity.CertificateError: If *certificate* contains invalid / unexpected data. This includes the case where the certificate contains no ``subjectAltName``\ s. .. versionadded:: 18.1.0 .. versionchanged:: 24.1.0 :exc:`~service_identity.CertificateError` is raised if the certificate contains no ``subjectAltName``\ s instead of :exc:`~service_identity.VerificationError`. """ verify_service_identity( cert_patterns=extract_patterns(certificate), obligatory_ids=[IPAddress_ID(ip_address)], optional_ids=[], ) ID_ON_DNS_SRV = ObjectIdentifier("1.3.6.1.5.5.7.8.7") # id_on_dnsSRV def extract_patterns(cert: Certificate) -> Sequence[CertificatePattern]: """ Extract all valid ID patterns from a certificate for service verification. Args: cert: The certificate to be dissected. Returns: List of IDs. .. versionchanged:: 23.1.0 ``commonName`` is not used as a fallback anymore. """ ids: list[CertificatePattern] = [] try: ext = cert.extensions.get_extension_for_oid( ExtensionOID.SUBJECT_ALTERNATIVE_NAME ) except ExtensionNotFound: pass else: ids.extend( [ DNSPattern.from_bytes(name.encode("utf-8")) for name in ext.value.get_values_for_type(DNSName) ] ) ids.extend( [ URIPattern.from_bytes(uri.encode("utf-8")) for uri in ext.value.get_values_for_type( UniformResourceIdentifier ) ] ) ids.extend( [ IPAddressPattern(ip) for ip in ext.value.get_values_for_type(IPAddress) ] ) for other in ext.value.get_values_for_type(OtherName): if other.type_id == ID_ON_DNS_SRV: srv, _ = decode(other.value) if isinstance(srv, IA5String): ids.append(SRVPattern.from_bytes(srv.asOctets())) else: # pragma: no cover msg = "Unexpected certificate content." raise CertificateError(msg) return ids def extract_ids(cert: Certificate) -> Sequence[CertificatePattern]: """ Deprecated and never public API. Use :func:`extract_patterns` instead. .. deprecated:: 23.1.0 """ warnings.warn( category=DeprecationWarning, message="`extract_ids()` is deprecated, please use `extract_patterns()`.", stacklevel=2, ) return extract_patterns(cert) service_identity-24.2.0/src/service_identity/exceptions.py0000644000000000000000000000275314707112756021012 0ustar00""" All exceptions and warnings thrown by ``service-identity``. Separated into an own package for nicer tracebacks, you should still import them from __init__.py. """ from __future__ import annotations from typing import TYPE_CHECKING, Sequence if TYPE_CHECKING: from .hazmat import ServiceID import attr class SubjectAltNameWarning(DeprecationWarning): """ This warning is not used anymore and will be removed in a future version. Formerly: Server Certificate does not contain a ``SubjectAltName``. Hostname matching is performed on the ``CommonName`` which is deprecated. .. deprecated:: 23.1.0 """ @attr.s(slots=True) class Mismatch: mismatched_id: ServiceID = attr.ib() class DNSMismatch(Mismatch): """ No matching DNSPattern could be found. """ class SRVMismatch(Mismatch): """ No matching SRVPattern could be found. """ class URIMismatch(Mismatch): """ No matching URIPattern could be found. """ class IPAddressMismatch(Mismatch): """ No matching IPAddressPattern could be found. """ @attr.s(auto_exc=True) class VerificationError(Exception): """ Service identity verification failed. """ errors: Sequence[Mismatch] = attr.ib() def __str__(self) -> str: return self.__repr__() class CertificateError(Exception): r""" Certificate contains invalid or unexpected data. This includes the case where s certificate contains no ``subjectAltName``\ s. """ service_identity-24.2.0/src/service_identity/hazmat.py0000644000000000000000000003070314707112756020111 0ustar00""" Common verification code. """ from __future__ import annotations import ipaddress import re from typing import Protocol, Sequence, Union, runtime_checkable import attr from .exceptions import ( CertificateError, DNSMismatch, IPAddressMismatch, Mismatch, SRVMismatch, URIMismatch, VerificationError, ) try: import idna except ImportError: idna = None # type: ignore[assignment] @attr.s(slots=True) class ServiceMatch: """ A match of a service id and a certificate pattern. """ service_id: ServiceID = attr.ib() cert_pattern: CertificatePattern = attr.ib() def verify_service_identity( cert_patterns: Sequence[CertificatePattern], obligatory_ids: Sequence[ServiceID], optional_ids: Sequence[ServiceID], ) -> list[ServiceMatch]: """ Verify whether *cert_patterns* are valid for *obligatory_ids* and *optional_ids*. *obligatory_ids* must be both present and match. *optional_ids* must match if a pattern of the respective type is present. """ if not cert_patterns: msg = "Certificate does not contain any `subjectAltName`s." raise CertificateError(msg) errors = [] matches = _find_matches(cert_patterns, obligatory_ids) + _find_matches( cert_patterns, optional_ids ) matched_ids = [match.service_id for match in matches] for i in obligatory_ids: if i not in matched_ids: errors.append( # noqa: PERF401 i.error_on_mismatch(mismatched_id=i) ) for i in optional_ids: # If an optional ID is not matched by a certificate pattern *but* there # is a pattern of the same type , it is an error and the verification # fails. Example: the user passes a SRV-ID for "_mail.domain.com" but # the certificate contains an SRV-Pattern for "_xmpp.domain.com". if i not in matched_ids and _contains_instance_of( cert_patterns, i.pattern_class ): errors.append( # noqa: PERF401 i.error_on_mismatch(mismatched_id=i) ) if errors: raise VerificationError(errors=errors) return matches def _find_matches( cert_patterns: Sequence[CertificatePattern], service_ids: Sequence[ServiceID], ) -> list[ServiceMatch]: """ Search for matching certificate patterns and service_ids. Args: service_ids: List of service IDs like DNS_ID. """ matches = [] for sid in service_ids: for cid in cert_patterns: if sid.verify(cid): matches.append( # noqa: PERF401 ServiceMatch(cert_pattern=cid, service_id=sid) ) return matches def _contains_instance_of(seq: Sequence[object], cl: type) -> bool: return any(isinstance(e, cl) for e in seq) def _is_ip_address(pattern: str | bytes) -> bool: """ Check whether *pattern* could be/match an IP address. Args: pattern: A pattern for a host name. Returns: `True` if *pattern* could be an IP address, else `False`. """ if isinstance(pattern, bytes): try: pattern = pattern.decode("ascii") except UnicodeError: return False try: int(pattern) except ValueError: pass else: return True try: ipaddress.ip_address(pattern.replace("*", "1")) except ValueError: return False return True @attr.s(slots=True) class DNSPattern: """ A DNS pattern as extracted from certificates. """ #: The pattern. pattern: bytes = attr.ib() _RE_LEGAL_CHARS = re.compile(rb"^[a-z0-9\-_.]+$") @classmethod def from_bytes(cls, pattern: bytes) -> DNSPattern: if not isinstance(pattern, bytes): msg = "The DNS pattern must be a bytes string." raise TypeError(msg) pattern = pattern.strip() if pattern == b"" or _is_ip_address(pattern) or b"\0" in pattern: msg = f"Invalid DNS pattern {pattern!r}." raise CertificateError(msg) pattern = pattern.translate(_TRANS_TO_LOWER) if b"*" in pattern: _validate_pattern(pattern) return cls(pattern=pattern) @attr.s(slots=True) class IPAddressPattern: """ An IP address pattern as extracted from certificates. """ #: The pattern. pattern: ipaddress.IPv4Address | ipaddress.IPv6Address = attr.ib() @classmethod def from_bytes(cls, bs: bytes) -> IPAddressPattern: try: return cls(pattern=ipaddress.ip_address(bs)) except ValueError: msg = f"Invalid IP address pattern {bs!r}." raise CertificateError(msg) from None @attr.s(slots=True) class URIPattern: """ An URI pattern as extracted from certificates. """ #: The pattern for the protocol part. protocol_pattern: bytes = attr.ib() #: The pattern for the DNS part. dns_pattern: DNSPattern = attr.ib() @classmethod def from_bytes(cls, pattern: bytes) -> URIPattern: if not isinstance(pattern, bytes): msg = "The URI pattern must be a bytes string." raise TypeError(msg) pattern = pattern.strip().translate(_TRANS_TO_LOWER) if b":" not in pattern or b"*" in pattern or _is_ip_address(pattern): msg = f"Invalid URI pattern {pattern!r}." raise CertificateError(msg) protocol_pattern, hostname = pattern.split(b":") return cls( protocol_pattern=protocol_pattern, dns_pattern=DNSPattern.from_bytes(hostname), ) @attr.s(slots=True) class SRVPattern: """ An SRV pattern as extracted from certificates. """ #: The pattern for the name part. name_pattern: bytes = attr.ib() #: The pattern for the DNS part. dns_pattern: DNSPattern = attr.ib() @classmethod def from_bytes(cls, pattern: bytes) -> SRVPattern: if not isinstance(pattern, bytes): msg = "The SRV pattern must be a bytes string." raise TypeError(msg) pattern = pattern.strip().translate(_TRANS_TO_LOWER) if ( pattern[0] != b"_"[0] or b"." not in pattern or b"*" in pattern or _is_ip_address(pattern) ): msg = f"Invalid SRV pattern {pattern!r}." raise CertificateError(msg) name, hostname = pattern.split(b".", 1) return cls( name_pattern=name[1:], dns_pattern=DNSPattern.from_bytes(hostname) ) CertificatePattern = Union[ SRVPattern, URIPattern, DNSPattern, IPAddressPattern ] """ A :class:`Union` of all possible patterns that can be extracted from a certificate. """ @runtime_checkable class ServiceID(Protocol): @property def pattern_class(self) -> type[CertificatePattern]: ... @property def error_on_mismatch(self) -> type[Mismatch]: ... def verify(self, pattern: CertificatePattern) -> bool: ... @attr.s(init=False, slots=True) class DNS_ID: """ A DNS service ID, aka hostname. """ hostname: bytes = attr.ib() # characters that are legal in a normalized hostname _RE_LEGAL_CHARS = re.compile(rb"^[a-z0-9\-_.]+$") pattern_class = DNSPattern error_on_mismatch = DNSMismatch def __init__(self, hostname: str): if not isinstance(hostname, str): msg = "DNS-ID must be a text string." raise TypeError(msg) hostname = hostname.strip() if not hostname or _is_ip_address(hostname): msg = "Invalid DNS-ID." raise ValueError(msg) if any(ord(c) > 127 for c in hostname): if idna: ascii_id = idna.encode(hostname) else: msg = "idna library is required for non-ASCII IDs." raise ImportError(msg) else: ascii_id = hostname.encode("ascii") self.hostname = ascii_id.translate(_TRANS_TO_LOWER) if self._RE_LEGAL_CHARS.match(self.hostname) is None: msg = "Invalid DNS-ID." raise ValueError(msg) def verify(self, pattern: CertificatePattern) -> bool: """ https://tools.ietf.org/search/rfc6125#section-6.4 """ if isinstance(pattern, self.pattern_class): return _hostname_matches(pattern.pattern, self.hostname) return False @attr.s(slots=True) class IPAddress_ID: """ An IP address service ID. """ ip: ipaddress.IPv4Address | ipaddress.IPv6Address = attr.ib( converter=ipaddress.ip_address ) pattern_class = IPAddressPattern error_on_mismatch = IPAddressMismatch def verify(self, pattern: CertificatePattern) -> bool: """ https://tools.ietf.org/search/rfc2818#section-3.1 """ if isinstance(pattern, self.pattern_class): return self.ip == pattern.pattern return False @attr.s(init=False, slots=True) class URI_ID: """ An URI service ID. """ protocol: bytes = attr.ib() dns_id: DNS_ID = attr.ib() pattern_class = URIPattern error_on_mismatch = URIMismatch def __init__(self, uri: str): if not isinstance(uri, str): msg = "URI-ID must be a text string." raise TypeError(msg) uri = uri.strip() if ":" not in uri or _is_ip_address(uri): msg = "Invalid URI-ID." raise ValueError(msg) prot, hostname = uri.split(":") self.protocol = prot.encode("ascii").translate(_TRANS_TO_LOWER) self.dns_id = DNS_ID(hostname.strip("/")) def verify(self, pattern: CertificatePattern) -> bool: """ https://tools.ietf.org/search/rfc6125#section-6.5.2 """ if isinstance(pattern, self.pattern_class): return ( pattern.protocol_pattern == self.protocol and self.dns_id.verify(pattern.dns_pattern) ) return False @attr.s(init=False, slots=True) class SRV_ID: """ An SRV service ID. """ name: bytes = attr.ib() dns_id: DNS_ID = attr.ib() pattern_class = SRVPattern error_on_mismatch = SRVMismatch def __init__(self, srv: str): if not isinstance(srv, str): msg = "SRV-ID must be a text string." raise TypeError(msg) srv = srv.strip() if "." not in srv or _is_ip_address(srv) or srv[0] != "_": msg = "Invalid SRV-ID." raise ValueError(msg) name, hostname = srv.split(".", 1) self.name = name[1:].encode("ascii").translate(_TRANS_TO_LOWER) self.dns_id = DNS_ID(hostname) def verify(self, pattern: CertificatePattern) -> bool: """ https://tools.ietf.org/search/rfc6125#section-6.5.1 """ if isinstance(pattern, self.pattern_class): return self.name == pattern.name_pattern and self.dns_id.verify( pattern.dns_pattern ) return False def _hostname_matches(cert_pattern: bytes, actual_hostname: bytes) -> bool: """ :return: `True` if *cert_pattern* matches *actual_hostname*, else `False`. """ if b"*" in cert_pattern: cert_head, cert_tail = cert_pattern.split(b".", 1) actual_head, actual_tail = actual_hostname.split(b".", 1) if cert_tail != actual_tail: return False # No patterns for IDNA if actual_head.startswith(b"xn--"): return False return cert_head in (b"*", actual_head) return cert_pattern == actual_hostname def _validate_pattern(cert_pattern: bytes) -> None: """ Check whether the usage of wildcards within *cert_pattern* conforms with our expectations. """ cnt = cert_pattern.count(b"*") if cnt > 1: msg = f"Certificate's DNS-ID {cert_pattern!r} contains too many wildcards." raise CertificateError(msg) parts = cert_pattern.split(b".") if len(parts) < 3: msg = f"Certificate's DNS-ID {cert_pattern!r} has too few host components for wildcard usage." raise CertificateError(msg) # We assume there will always be only one wildcard allowed. if b"*" not in parts[0]: msg = f"Certificate's DNS-ID {cert_pattern!r} has a wildcard outside the left-most part." raise CertificateError(msg) if any(not len(p) for p in parts): msg = f"Certificate's DNS-ID {cert_pattern!r} contains empty parts." raise CertificateError(msg) # Ensure no locale magic interferes. _TRANS_TO_LOWER = bytes.maketrans( b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", b"abcdefghijklmnopqrstuvwxyz" ) service_identity-24.2.0/src/service_identity/py.typed0000644000000000000000000000000014707112756017735 0ustar00service_identity-24.2.0/src/service_identity/pyopenssl.py0000644000000000000000000000714314707112756020663 0ustar00""" `pyOpenSSL `_-specific code. """ from __future__ import annotations import contextlib import warnings from typing import Sequence from .cryptography import extract_patterns as _cryptography_extract_patterns from .hazmat import ( DNS_ID, CertificatePattern, IPAddress_ID, verify_service_identity, ) with contextlib.suppress(ImportError): # We only use it for docstrings -- `if TYPE_CHECKING`` does not work. from OpenSSL.crypto import X509 from OpenSSL.SSL import Connection __all__ = ["verify_hostname"] def verify_hostname(connection: Connection, hostname: str) -> None: r""" Verify whether the certificate of *connection* is valid for *hostname*. Args: connection: A pyOpenSSL connection object. hostname: The hostname that *connection* should be connected to. Raises: service_identity.VerificationError: If *connection* does not provide a certificate that is valid for *hostname*. service_identity.CertificateError: If certificate provided by *connection* contains invalid / unexpected data. This includes the case where the certificate contains no ``subjectAltName``\ s. .. versionchanged:: 24.1.0 :exc:`~service_identity.CertificateError` is raised if the certificate contains no ``subjectAltName``\ s instead of :exc:`~service_identity.VerificationError`. """ verify_service_identity( cert_patterns=extract_patterns( connection.get_peer_certificate() # type:ignore[arg-type] ), obligatory_ids=[DNS_ID(hostname)], optional_ids=[], ) def verify_ip_address(connection: Connection, ip_address: str) -> None: r""" Verify whether the certificate of *connection* is valid for *ip_address*. Args: connection: A pyOpenSSL connection object. ip_address: The IP address that *connection* should be connected to. Can be an IPv4 or IPv6 address. Raises: service_identity.VerificationError: If *connection* does not provide a certificate that is valid for *ip_address*. service_identity.CertificateError: If the certificate chain of *connection* contains a certificate that contains invalid/unexpected data. .. versionadded:: 18.1.0 .. versionchanged:: 24.1.0 :exc:`~service_identity.CertificateError` is raised if the certificate contains no ``subjectAltName``\ s instead of :exc:`~service_identity.VerificationError`. """ verify_service_identity( cert_patterns=extract_patterns( connection.get_peer_certificate() # type:ignore[arg-type] ), obligatory_ids=[IPAddress_ID(ip_address)], optional_ids=[], ) def extract_patterns(cert: X509) -> Sequence[CertificatePattern]: """ Extract all valid ID patterns from a certificate for service verification. Args: cert: The certificate to be dissected. Returns: List of IDs. .. versionchanged:: 23.1.0 ``commonName`` is not used as a fallback anymore. """ return _cryptography_extract_patterns(cert.to_cryptography()) def extract_ids(cert: X509) -> Sequence[CertificatePattern]: """ Deprecated and never public API. Use :func:`extract_patterns` instead. .. deprecated:: 23.1.0 """ warnings.warn( category=DeprecationWarning, message="`extract_ids()` is deprecated, please use `extract_patterns()`.", stacklevel=2, ) return extract_patterns(cert) service_identity-24.2.0/tests/__init__.py0000644000000000000000000000000014707112756015351 0ustar00service_identity-24.2.0/tests/certificates.py0000644000000000000000000001706714707112756016304 0ustar00from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_pem_x509_certificate from service_identity.cryptography import extract_patterns # Test certificates PEM_DNS_ONLY = b"""\ -----BEGIN CERTIFICATE----- MIIGbjCCBVagAwIBAgIDCesrMA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0 YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg MSBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTMwNDEwMTk1ODA5 WhcNMTQwNDExMTkyODAwWjB1MRkwFwYDVQQNExBTN2xiQ3Q3TjJSNHQ5bzhKMQsw CQYDVQQGEwJVUzEeMBwGA1UEAxMVd3d3LnR3aXN0ZWRtYXRyaXguY29tMSswKQYJ KoZIhvcNAQkBFhxwb3N0bWFzdGVyQHR3aXN0ZWRtYXRyaXguY29tMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxUH8iDxIEiDcMQb8kr/JTYXDGuE8ISQA uw/gBqpvHIvCgPBkZpvjQLA23rnUZm1S3VG5MIq6gZVdtl9LFIfokMPGgY9EZng8 BaI+6Y36cMtubnzW53OZb7yLQQyg+rjuwjvJOY33ZulEthxhdB3km1Leb67iE9v7 dpyKeJ/8m2IWD37HCtXIEnp9ZqWOZkAPzlzDt6oNxj0s/l3z23+XqZdr+kmlh9U+ VWBTPppO4AJNwSqbBd0PgIozbYsp6urxSr40YQkIYFOOZQNs7HETJE71Ia7DQcUD kUF1jZSYZnhVQwGPisqQLGodt9q9p2BhpSf0cUm02uKKzYi5A2h7UQIDAQABo4IC 7TCCAukwCQYDVR0TBAIwADALBgNVHQ8EBAMCA6gwEwYDVR0lBAwwCgYIKwYBBQUH AwEwHQYDVR0OBBYEFGeuUvDrFHkl7Krl/+rlv1FsnsU6MB8GA1UdIwQYMBaAFOtC NNCYsKuf9BtrCPfMZC7vDixFMDMGA1UdEQQsMCqCFXd3dy50d2lzdGVkbWF0cml4 LmNvbYIRdHdpc3RlZG1hdHJpeC5jb20wggFWBgNVHSAEggFNMIIBSTAIBgZngQwB AgEwggE7BgsrBgEEAYG1NwECAzCCASowLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cu c3RhcnRzc2wuY29tL3BvbGljeS5wZGYwgfcGCCsGAQUFBwICMIHqMCcWIFN0YXJ0 Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MAMCAQEagb5UaGlzIGNlcnRpZmlj YXRlIHdhcyBpc3N1ZWQgYWNjb3JkaW5nIHRvIHRoZSBDbGFzcyAxIFZhbGlkYXRp b24gcmVxdWlyZW1lbnRzIG9mIHRoZSBTdGFydENvbSBDQSBwb2xpY3ksIHJlbGlh bmNlIG9ubHkgZm9yIHRoZSBpbnRlbmRlZCBwdXJwb3NlIGluIGNvbXBsaWFuY2Ug b2YgdGhlIHJlbHlpbmcgcGFydHkgb2JsaWdhdGlvbnMuMDUGA1UdHwQuMCwwKqAo oCaGJGh0dHA6Ly9jcmwuc3RhcnRzc2wuY29tL2NydDEtY3JsLmNybDCBjgYIKwYB BQUHAQEEgYEwfzA5BggrBgEFBQcwAYYtaHR0cDovL29jc3Auc3RhcnRzc2wuY29t L3N1Yi9jbGFzczEvc2VydmVyL2NhMEIGCCsGAQUFBzAChjZodHRwOi8vYWlhLnN0 YXJ0c3NsLmNvbS9jZXJ0cy9zdWIuY2xhc3MxLnNlcnZlci5jYS5jcnQwIwYDVR0S BBwwGoYYaHR0cDovL3d3dy5zdGFydHNzbC5jb20vMA0GCSqGSIb3DQEBBQUAA4IB AQCN85dUStYjHmWdXthpAqJcS3KD2JP6N9egOz7FTcToXLW8Kl5a2SUVaJv8Fzs+ wtbPJQSm0LyGtfdrR6iKFPf28Vm/VkYXPiOV08GD9B7yl1SjktXOsGMPlOHU8YQZ DEsHOrRvaZBSA1VtBQjYnoO0pDVu9QwDLAPLFvFice2PN803HuMFIwcuQSIrh4nq PqwitBZ6nPPHz7aSiAut/+txK3EZll0d+hl0H3Phd+ICeITYhNkLe90k7l1IFpET fJiBDvG/iDAJISgkrR1heuX/e+yWfx7RvqGlMLIE35d+0MhWy92Jzejbl8fJdr4C Kulh/pV07MWAUZxscUPtWmPo -----END CERTIFICATE-----""" DNS_IDS = extract_patterns( load_pem_x509_certificate(PEM_DNS_ONLY, default_backend()) ) PEM_CN_ONLY = b"""\ -----BEGIN CERTIFICATE----- MIIDlzCCAn+gAwIBAgIUNS9qfJRgoLrr68mAfmhzBior0JAwDQYJKoZIhvcNAQEL BQAwWzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20w HhcNMjMwNjA4MDkwNDAxWhcNNDkwMTI3MDkwNDAxWjBbMQswCQYDVQQGEwJBVTET MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ dHkgTHRkMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBALltBNRAyeFczK2kdKTkW6E3/+rptXBcaw3SyltY/Ft8DPe3 lW/GWbbgAU7ceYC0LkYxOEnyQTlXhTyLHSk+PCQLi2ESgwWGnGY5Eusk1DRJ162q hLUkmz25KdTILnh402fgoziF2RU6PnA7iIwAVntT5uh4S1yPW7TWkdPsE0tc1yBx 3SAHTXDkoahjKSot35Q87Jw92fxUd7WqVmDrgcLMvp8rXNjdg+HG4fQGoMcSQq2Z nVPIoWSTOGdL6SzSs6Wvllz3n7YWShTqp9CmGctCGubt02fHF+8G/dMTTukntG4q EjBtVfeDFx8yXUdOgN4egFxZGYATAUsLcyzNhQcCAwEAAaNTMFEwHQYDVR0OBBYE FDkuqwU8KxIvwAwMqVpNFYlgd6WbMB8GA1UdIwQYMBaAFDkuqwU8KxIvwAwMqVpN FYlgd6WbMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJ9Nb/C0 2gkxhWm3S2/9onPMhFOxDLsYxBvZtJMeOvRRrJaxN2m0aXkm0Ww8Bq0qEegZE51m 0T57IOS256rQlxEnZNeAWT2tf8FEqQGjbI1c/prj420e6afn18x8CSQgiOG4PJ+S YfufRjSHAYwiiNi+tiKBQ8jOuayFhih9/GsDJvAMW0HjaYZyG6jlA9p4bL1rSqD3 gE+upYhkpVN1lFRZAhxRZOVIqJV/ppHp9RAuawsUdSNKm+rnlaExJKoVys+tZL9+ djWWQVqnttYocQQ0umX4ydCTr1RM+7ziwWTR5kpOESA/gZePZQqeH9k+XrPEQtLn 6QBxk+Ft53ZPVso= -----END CERTIFICATE----- """ PEM_OTHER_NAME = b"""\ -----BEGIN CERTIFICATE----- MIID/DCCAuSgAwIBAgIJAIS0TSddIw6cMA0GCSqGSIb3DQEBBQUAMGwxFDASBgNV BAMTC2V4YW1wbGUuY29tMSAwHgYJKoZIhvcNAQkBFhFib2d1c0BleGFtcGxlLmNv bTEUMBIGA1UEChMLRXhhbXBsZSBJbmMxDzANBgNVBAcTBkJlcmxpbjELMAkGA1UE BhMCREUwHhcNMTQwMzA2MTYyNTA5WhcNMTUwMzA2MTYyNTA5WjBsMRQwEgYDVQQD EwtleGFtcGxlLmNvbTEgMB4GCSqGSIb3DQEJARYRYm9ndXNAZXhhbXBsZS5jb20x FDASBgNVBAoTC0V4YW1wbGUgSW5jMQ8wDQYDVQQHEwZCZXJsaW4xCzAJBgNVBAYT AkRFMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxGQUcOc8cAdzSJbk 0eCHA1qBY2XwRG8YQzihgQS8Ey+3j69Xf0mtWOlL6v23v8J1ilA7ERs87Y4nbV/9 GJVhC/jTMZmrC6ogwtVIl1wL8sTiHaQZ/4pbpx57YW3qCdefLQrZqAMUgAe20z0G YVU97u5EGXHYahG4TnB3xN6Qd3BGKP7K69Lb7ZOES2Esq533AZxZShseYR4JNYAc 2anag2/DpHw6k8ZaxtWHR4SmxlkCoW5IPK0YypeUY91PFY+dxJQEewtisfALKltE SYnOTWkc0K9YuLuYVogx0K285wX4/Yha2wyo6KSAm0txJayOhcrEP2/34aWCl62m xOtPbQIDAQABo4GgMIGdMIGaBgNVHREEgZIwgY+CDSouZXhhbXBsZS5uZXSCC2V4 YW1wbGUuY29thwTAqAABhxAAEwAAAAAAAAAAAAAAAAAXhhNodHRwOi8vZXhhbXBs ZS5jb20voCYGCCsGAQUFBwgHoBoWGF94bXBwLWNsaWVudC5leGFtcGxlLm5ldKAc BggrBgEFBQcIBaAQDA5pbS5leGFtcGxlLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEA ACVQcgEKzXEw0M9mmVFFXL2SyDk/4oaDFZbnNfyUp+H7bnxdVBG2M3DzQQLw5yH5 k4GNPvHOKshBbaFcZWiG1sdrfQJy/UjIWnaC5410npfBv7kJWafKKxZzMq3gp4rd jPO2LxuWcYVOnUtA3CBe12tRV7ynGU8KmKOsU9bOWhUKo8DJ4a6XHB+YwXeOTPyU mG7XBpQebT01I3OijFJ+apKR2ubjwZE8l1+BAlTzHyUmmcTTWTQk8FTFcP3nZuIr VyudDBMASs4yVGHzQxmMalYYzd7ZDzM1NrgfG1KyKWqZEA0MzUxiYdUbZN79xL52 EyKUOXPHw78G6zsVmAE1Aw== -----END CERTIFICATE-----""" PEM_EVERYTHING = b"""\ -----BEGIN CERTIFICATE----- MIIGdTCCBF2gAwIBAgIUSxHQCcw8po0mpISRHmijCA7HF9YwDQYJKoZIhvcNAQEL BQAwEzERMA8GA1UEAxMIY2Eudm0uYWcwHhcNMTgwMjExMTMxOTI0WhcNMTgwMjEx MTMyMDQyWjAjMSEwHwYDVQQDExhzZXJ2aWNlLmlkZW50aXR5LmludmFsaWQwggIi MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzYDZKBb91iX9Ct8gFig//2UtA fRiDdiViimnAuLJ3f4Q5rM2Xs4BQpXEGgf4tBeZ03lFIga+W7nzsNZnooN6ocLwB z3jb3K4xxy1RRzv0iKLhFdtQwfwS6Xz6usaySGWW5Hpn3Yqwd9qAho/MIFfDruuL kEInjhtJGta/uT3fZ9BiLsDl1zyZefvhLblpujww5Ex3eGHgZlLixfuQj+vZbQ99 2xMHRIh6PRVsnVJ7GaSxxIwAdXcVZRuB4he3aIIn8OMCf+1V5aUTfC5vWVrSFfJb B1V9uw4DB0Uf/bn8bkm4ncr11kjiOUoNahXwPanHVFkTyr2hDU/SguIPRBGFBFCC RRUbsEhpJrtKy4mc1RQzof+fMJqmTjvRGoIYISfpuL3B84UBuXB6bWoKqsIrsX+Z Ww3bO7/ncpgko7zQSpjPUxAJQ2z/u+aCh/v++UudMGtYtQlBNTtkQsIAAaho/vHF ALjusQKj8J6LLJXWrNW0MzidgookHBu3cjE++ymK9bKsgbUFH+T1hf9WIaFR0ldY uCyOiDx7wxqV8KS3/FXAFU5ra6HtNVy67umcL+e8frBFABxdHu0SWNnXRN5qF233 WQ/0ds0KjjPC19+fH/KlwVuK4u725dtbeKmbbfeqrUhCoDVLG2xfIEPDrwfNiuRx n//9JahPtu53aRN7NwIDAQABo4IBrzCCAaswDgYDVR0PAQH/BAQDAgOoMB0GA1Ud JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUkrzVHzWYgC6iuhYm G91tbFg175YwHwYDVR0jBBgwFoAUZDPQAysTTYandW8IzZhkZMamUs0wRQYIKwYB BQUHAQEEOTA3MDUGCCsGAQUFBzAChilodHRwczovL2NhLnZtLmFnOjgyMDAvdjEv dm1jYV9pbnRtX3BraS9jYTCBtQYDVR0RBIGtMIGqghhzZXJ2aWNlLmlkZW50aXR5 LmludmFsaWSCIyoud2lsZGNhcmQuc2VydmljZS5pZGVudGl0eS5pbnZhbGlkghhz ZXJ2aWNlLmlkZW50aXR5LmludmFsaWSCH3NpbmdsZS5zZXJ2aWNlLmlkZW50aXR5 LmludmFsaWSHBAEBAQGHEAAAAAAAAAAAAAAAAAAAAAGHBAICAgKHECoAHDgAAAAA AAAAAAAAAFMwOwYDVR0fBDQwMjAwoC6gLIYqaHR0cHM6Ly9jYS52bS5hZzo4MjAw L3YxL3ZtY2FfaW50bV9wa2kvY3JsMA0GCSqGSIb3DQEBCwUAA4ICAQA6fR0V39IN zqFkJFUFyt/uX7aMnMbe2DKxXmhJns6VwN+nhzB4CNK5rSJ0y0telN5CL2Oe+pS/ Vfinw15GrdB01r9mV/og0aFMyXFUjmDa4heNKvbuspj+hHjXj2JvETk9pHKURmQe kd0IffkoDaSFIwjI0rOdDdo+5WcFpjx8lq8IZeBcPdVhqlzIaNa/PgezUg69HQF/ FEqBkaq4sto8/yXrD6Pp5NszRJvBtEnlq+WSYzvVSH6E48KD1sJr2DTGWs8pi9ml 7exq1yRSlmz5bgOvl7AVGrl+icOuCpDcuVgE2MbzKm/VKQ01ypUPvnUcDZJC8iDC 6JNT152YuLY/rgq+XJMeLb/FtDKmav8oOWqeoD72baMub9iVlZ4kaMzjtMFlXVha 6MQiV36QG99q8KPdxeRxuef4p3NRFa8AlFGbOa/ALxksN9rr8fPxAaNrHBzYsCgN DZoyYaYe6aIx8wVtpbucdinDSyn7aJy66RHUnKNwW/tJm3WXCI492dEX+s7PGVXA F4B0w+r2LTELSYJ6Mh+tVleuJZ6Yw947E4iAyc/u7ck6qWRex230hnHZqgRiexP2 4ZueMI+SnpWqL7rOgLD6VuyemZ18on2VJcgvZiVkYMfZf2330ZlRxtyU2AvKRXc3 3HotzNMgpPpx8C2KKLKKaiIGRY0pg/WC6w== -----END CERTIFICATE-----""" service_identity-24.2.0/tests/conftest.py0000644000000000000000000000103314707112756015446 0ustar00import cryptography try: import OpenSSL import OpenSSL.SSL except ImportError: OpenSSL = None def pytest_report_header(config): if OpenSSL is not None: openssl_version = OpenSSL.SSL.SSLeay_version( OpenSSL.SSL.SSLEAY_VERSION ).decode("ascii") pyopenssl_version = OpenSSL.__version__ else: openssl_version = "n/a" pyopenssl_version = "missing" return f"""\ OpenSSL: {openssl_version} pyOpenSSL: {pyopenssl_version} cryptography: {cryptography.__version__}""" service_identity-24.2.0/tests/test_cryptography.py0000644000000000000000000001221214707112756017414 0ustar00import ipaddress import pytest from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_pem_x509_certificate from service_identity.cryptography import ( extract_ids, extract_patterns, verify_certificate_hostname, verify_certificate_ip_address, ) from service_identity.exceptions import ( CertificateError, DNSMismatch, IPAddressMismatch, VerificationError, ) from service_identity.hazmat import ( DNS_ID, DNSPattern, IPAddress_ID, IPAddressPattern, URIPattern, ) from .certificates import ( PEM_CN_ONLY, PEM_DNS_ONLY, PEM_EVERYTHING, PEM_OTHER_NAME, ) backend = default_backend() X509_DNS_ONLY = load_pem_x509_certificate(PEM_DNS_ONLY, backend) X509_CN_ONLY = load_pem_x509_certificate(PEM_CN_ONLY, backend) X509_OTHER_NAME = load_pem_x509_certificate(PEM_OTHER_NAME, backend) CERT_EVERYTHING = load_pem_x509_certificate(PEM_EVERYTHING, backend) class TestPublicAPI: def test_no_cert_patterns_hostname(self): """ A certificate without subjectAltNames raises a helpful CertificateError. """ with pytest.raises( CertificateError, match="Certificate does not contain any `subjectAltName`s.", ): verify_certificate_hostname(X509_CN_ONLY, "example.com") @pytest.mark.parametrize("ip", ["203.0.113.0", "2001:db8::"]) def test_no_cert_patterns_ip_address(self, ip): """ A certificate without subjectAltNames raises a helpful CertificateError. """ with pytest.raises( CertificateError, match="Certificate does not contain any `subjectAltName`s.", ): verify_certificate_ip_address(X509_CN_ONLY, ip) def test_certificate_verify_hostname_ok(self): """ verify_certificate_hostname succeeds if the hostnames match. """ verify_certificate_hostname(X509_DNS_ONLY, "twistedmatrix.com") def test_certificate_verify_hostname_fail(self): """ verify_certificate_hostname fails if the hostnames don't match and provides the user with helpful information. """ with pytest.raises(VerificationError) as ei: verify_certificate_hostname(X509_DNS_ONLY, "google.com") assert [ DNSMismatch(mismatched_id=DNS_ID("google.com")) ] == ei.value.errors @pytest.mark.parametrize("ip", ["1.1.1.1", "::1"]) def test_verify_certificate_ip_address_ok(self, ip): """ verify_certificate_ip_address succeeds if the addresses match. Works both with IPv4 and IPv6. """ verify_certificate_ip_address(CERT_EVERYTHING, ip) @pytest.mark.parametrize("ip", ["1.1.1.2", "::2"]) def test_verify_ip_address_fail(self, ip): """ verify_ip_address fails if the addresses don't match and provides the user with helpful information. Works both with IPv4 and IPv6. """ with pytest.raises(VerificationError) as ei: verify_certificate_ip_address(CERT_EVERYTHING, ip) assert [ IPAddressMismatch(mismatched_id=IPAddress_ID(ip)) ] == ei.value.errors class TestExtractPatterns: def test_dns(self): """ Returns the correct DNSPattern from a certificate. """ rv = extract_patterns(X509_DNS_ONLY) assert [ DNSPattern.from_bytes(b"www.twistedmatrix.com"), DNSPattern.from_bytes(b"twistedmatrix.com"), ] == rv def test_cn_ids_are_ignored(self): """ commonName is not supported anymore and therefore ignored. """ assert [] == extract_patterns(X509_CN_ONLY) def test_uri(self): """ Returns the correct URIPattern from a certificate. """ rv = extract_patterns(X509_OTHER_NAME) assert [URIPattern.from_bytes(b"http://example.com/")] == [ id for id in rv if isinstance(id, URIPattern) ] def test_ip(self): """ Returns IP patterns. """ rv = extract_patterns(CERT_EVERYTHING) assert [ DNSPattern.from_bytes(pattern=b"service.identity.invalid"), DNSPattern.from_bytes( pattern=b"*.wildcard.service.identity.invalid" ), DNSPattern.from_bytes(pattern=b"service.identity.invalid"), DNSPattern.from_bytes(pattern=b"single.service.identity.invalid"), IPAddressPattern(pattern=ipaddress.IPv4Address("1.1.1.1")), IPAddressPattern(pattern=ipaddress.IPv6Address("::1")), IPAddressPattern(pattern=ipaddress.IPv4Address("2.2.2.2")), IPAddressPattern(pattern=ipaddress.IPv6Address("2a00:1c38::53")), ] == rv def test_extract_ids_deprecated(self): """ `extract_ids` raises a DeprecationWarning with correct stacklevel. """ with pytest.deprecated_call() as wr: extract_ids(CERT_EVERYTHING) w = wr.pop() assert ( "`extract_ids()` is deprecated, please use `extract_patterns()`." == w.message.args[0] ) assert __file__ == w.filename service_identity-24.2.0/tests/test_hazmat.py0000644000000000000000000005164014707112756016155 0ustar00import ipaddress import pickle import pytest import service_identity.hazmat from service_identity.cryptography import extract_patterns from service_identity.exceptions import ( CertificateError, DNSMismatch, SRVMismatch, VerificationError, ) from service_identity.hazmat import ( DNS_ID, SRV_ID, URI_ID, DNSPattern, IPAddress_ID, IPAddressPattern, ServiceMatch, SRVPattern, URIPattern, _contains_instance_of, _find_matches, _hostname_matches, _is_ip_address, _validate_pattern, verify_service_identity, ) from .certificates import DNS_IDS from .test_cryptography import CERT_EVERYTHING try: import idna except ImportError: idna = None class TestVerifyServiceIdentity: """ Simple integration tests for verify_service_identity. """ def test_no_cert_patterns(self): """ Empty cert patterns raise a helpful CertificateError. """ with pytest.raises( CertificateError, match="Certificate does not contain any `subjectAltName`s.", ): verify_service_identity( cert_patterns=[], obligatory_ids=[], optional_ids=[] ) def test_dns_id_success(self): """ Return pairs of certificate ids and service ids on matches. """ rv = verify_service_identity( DNS_IDS, [DNS_ID("twistedmatrix.com")], [] ) assert [ ServiceMatch( cert_pattern=DNSPattern.from_bytes(b"twistedmatrix.com"), service_id=DNS_ID("twistedmatrix.com"), ) ] == rv def test_integration_dns_id_fail(self): """ Raise VerificationError if no certificate id matches the supplied service ids. """ i = DNS_ID("wrong.host") with pytest.raises(VerificationError) as e: verify_service_identity( DNS_IDS, obligatory_ids=[i], optional_ids=[] ) assert [DNSMismatch(mismatched_id=i)] == e.value.errors def test_ip_address_success(self): """ IP addresses patterns are matched against IP address IDs. """ ip4 = ipaddress.ip_address("2.2.2.2") ip6 = ipaddress.ip_address("2a00:1c38::53") id4 = IPAddress_ID(str(ip4)) id6 = IPAddress_ID(str(ip6)) rv = verify_service_identity( extract_patterns(CERT_EVERYTHING), [id4, id6], [] ) assert [ ServiceMatch(id4, IPAddressPattern(ip4)), ServiceMatch(id6, IPAddressPattern(ip6)), ] == rv def test_obligatory_missing(self): """ Raise if everything matches but one of the obligatory IDs is missing. """ i = DNS_ID("example.net") with pytest.raises(VerificationError) as e: verify_service_identity( [SRVPattern.from_bytes(b"_mail.example.net")], obligatory_ids=[SRV_ID("_mail.example.net"), i], optional_ids=[], ) assert [DNSMismatch(mismatched_id=i)] == e.value.errors def test_obligatory_mismatch(self): """ Raise if one of the obligatory IDs doesn't match. """ i = DNS_ID("example.net") with pytest.raises(VerificationError) as e: verify_service_identity( [ SRVPattern.from_bytes(b"_mail.example.net"), DNSPattern.from_bytes(b"example.com"), ], obligatory_ids=[SRV_ID("_mail.example.net"), i], optional_ids=[], ) assert [DNSMismatch(mismatched_id=i)] == e.value.errors def test_optional_missing(self): """ Optional IDs may miss as long as they don't conflict with an existing pattern. """ p = DNSPattern.from_bytes(b"mail.foo.com") i = DNS_ID("mail.foo.com") rv = verify_service_identity( [p], obligatory_ids=[i], optional_ids=[SRV_ID("_mail.foo.com")] ) assert [ServiceMatch(cert_pattern=p, service_id=i)] == rv def test_optional_mismatch(self): """ Raise VerificationError if an ID from optional_ids does not match a pattern of respective type even if obligatory IDs match. """ i = SRV_ID("_xmpp.example.com") with pytest.raises(VerificationError) as e: verify_service_identity( [ DNSPattern.from_bytes(b"example.net"), SRVPattern.from_bytes(b"_mail.example.com"), ], obligatory_ids=[DNS_ID("example.net")], optional_ids=[i], ) assert [SRVMismatch(mismatched_id=i)] == e.value.errors def test_contains_optional_and_matches(self): """ If an optional ID is found, return the match within the returned list and don't raise an error. """ p = SRVPattern.from_bytes(b"_mail.example.net") i = SRV_ID("_mail.example.net") rv = verify_service_identity( [DNSPattern.from_bytes(b"example.net"), p], obligatory_ids=[DNS_ID("example.net")], optional_ids=[i], ) assert ServiceMatch(cert_pattern=p, service_id=i) == rv[1] class TestContainsInstance: def test_positive(self): """ If the list contains an object of the type, return True. """ assert _contains_instance_of([object(), (), object()], tuple) def test_negative(self): """ If the list does not contain an object of the type, return False. """ assert not _contains_instance_of([object(), [], {}], tuple) class TestDNS_ID: def test_enforces_unicode(self): """ Raise TypeError if pass DNS-ID is not unicode. """ with pytest.raises(TypeError): DNS_ID(b"foo.com") def test_handles_missing_idna(self, monkeypatch): """ Raise ImportError if idna is missing and a non-ASCII DNS-ID is passed. """ monkeypatch.setattr(service_identity.hazmat, "idna", None) with pytest.raises(ImportError): DNS_ID("f\xf8\xf8.com") def test_ascii_works_without_idna(self, monkeypatch): """ 7bit-ASCII DNS-IDs work no matter whether idna is present or not. """ monkeypatch.setattr(service_identity.hazmat, "idna", None) dns = DNS_ID("foo.com") assert b"foo.com" == dns.hostname @pytest.mark.skipif(idna is None, reason="idna not installed") def test_idna_used_if_available_on_non_ascii(self): """ If idna is installed and a non-ASCII DNS-ID is passed, encode it to ASCII. """ dns = DNS_ID("f\xf8\xf8.com") assert b"xn--f-5gaa.com" == dns.hostname @pytest.mark.parametrize( "invalid_id", [ " ", "", # empty strings "host,name", # invalid chars "192.168.0.0", "::1", "1234", # IP addresses ], ) def test_catches_invalid_dns_ids(self, invalid_id): """ Raise ValueError on invalid DNS-IDs. """ with pytest.raises(ValueError): DNS_ID(invalid_id) def test_lowercases(self): """ The hostname is lowercased so it can be compared case-insensitively. """ dns_id = DNS_ID("hOsTnAmE") assert b"hostname" == dns_id.hostname def test_verifies_only_dns(self): """ If anything else than DNSPattern is passed to verify, return False. """ assert not DNS_ID("foo.com").verify(object()) def test_simple_match(self): """ Simple integration test with _hostname_matches with a match. """ assert DNS_ID("foo.com").verify(DNSPattern.from_bytes(b"foo.com")) def test_simple_mismatch(self): """ Simple integration test with _hostname_matches with a mismatch. """ assert not DNS_ID("foo.com").verify(DNSPattern.from_bytes(b"bar.com")) def test_matches(self): """ Valid matches return `True`. """ for cert, actual in [ (b"www.example.com", b"www.example.com"), (b"*.example.com", b"www.example.com"), ]: assert _hostname_matches(cert, actual) def test_mismatches(self): """ Invalid matches return `False`. """ for cert, actual in [ (b"xxx.example.com", b"www.example.com"), (b"*.example.com", b"baa.foo.example.com"), (b"f*.example.com", b"baa.example.com"), (b"*.bar.com", b"foo.baz.com"), (b"*.bar.com", b"bar.com"), (b"x*.example.com", b"xn--gtter-jua.example.com"), (b"xxx*.example.com", b"xxxwww.example.com"), (b"f*.example.com", b"foo.example.com"), (b"*oo.bar.com", b"foo.bar.com"), (b"fo*oo.bar.com", b"fooooo.bar.com"), ]: assert not _hostname_matches(cert, actual) class TestURI_ID: def test_enforces_unicode(self): """ Raise TypeError if pass URI-ID is not unicode. """ with pytest.raises(TypeError): URI_ID(b"sip:foo.com") def test_create_DNS_ID(self): """ The hostname is converted into a DNS_ID object. """ uri_id = URI_ID("sip:foo.com") assert DNS_ID("foo.com") == uri_id.dns_id assert b"sip" == uri_id.protocol def test_lowercases(self): """ The protocol is lowercased so it can be compared case-insensitively. """ uri_id = URI_ID("sIp:foo.com") assert b"sip" == uri_id.protocol def test_catches_missing_colon(self): """ Raise ValueError if there's no colon within a URI-ID. """ with pytest.raises(ValueError): URI_ID("sip;foo.com") def test_is_only_valid_for_uri(self): """ If anything else than an URIPattern is passed to verify, return False. """ assert not URI_ID("sip:foo.com").verify(object()) def test_protocol_mismatch(self): """ If protocol doesn't match, verify returns False. """ assert not URI_ID("sip:foo.com").verify( URIPattern.from_bytes(b"xmpp:foo.com") ) def test_dns_mismatch(self): """ If the hostname doesn't match, verify returns False. """ assert not URI_ID("sip:bar.com").verify( URIPattern.from_bytes(b"sip:foo.com") ) def test_match(self): """ Accept legal matches. """ assert URI_ID("sip:foo.com").verify( URIPattern.from_bytes(b"sip:foo.com") ) class TestSRV_ID: def test_enforces_unicode(self): """ Raise TypeError if pass srv-ID is not unicode. """ with pytest.raises(TypeError): SRV_ID(b"_mail.example.com") def test_create_DNS_ID(self): """ The hostname is converted into a DNS_ID object. """ srv_id = SRV_ID("_mail.example.com") assert DNS_ID("example.com") == srv_id.dns_id def test_lowercases(self): """ The service name is lowercased so it can be compared case-insensitively. """ srv_id = SRV_ID("_MaIl.foo.com") assert b"mail" == srv_id.name def test_catches_missing_dot(self): """ Raise ValueError if there's no dot within a SRV-ID. """ with pytest.raises(ValueError): SRV_ID("_imapsfoocom") def test_catches_missing_underscore(self): """ Raise ValueError if the service is doesn't start with an underscore. """ with pytest.raises(ValueError): SRV_ID("imaps.foo.com") def test_is_only_valid_for_SRV(self): """ If anything else than an SRVPattern is passed to verify, return False. """ assert not SRV_ID("_mail.foo.com").verify(object()) def test_match(self): """ Accept legal matches. """ assert SRV_ID("_mail.foo.com").verify( SRVPattern.from_bytes(b"_mail.foo.com") ) @pytest.mark.skipif(idna is None, reason="idna not installed") def test_match_idna(self): """ IDNAs are handled properly. """ assert SRV_ID("_mail.f\xf8\xf8.com").verify( SRVPattern.from_bytes(b"_mail.xn--f-5gaa.com") ) def test_mismatch_service_name(self): """ If the service name doesn't match, verify returns False. """ assert not ( SRV_ID("_mail.foo.com").verify( SRVPattern.from_bytes(b"_xmpp.foo.com") ) ) def test_mismatch_dns(self): """ If the dns_id doesn't match, verify returns False. """ assert not ( SRV_ID("_mail.foo.com").verify( SRVPattern.from_bytes(b"_mail.bar.com") ) ) class TestDNSPattern: def test_enforces_bytes(self): """ Raise TypeError if unicode is passed. """ with pytest.raises(TypeError): DNSPattern.from_bytes("foo.com") def test_catches_empty(self): """ Empty DNS-IDs raise a :class:`CertificateError`. """ with pytest.raises(CertificateError): DNSPattern.from_bytes(b" ") def test_catches_NULL_bytes(self): """ Raise :class:`CertificateError` if a NULL byte is in the hostname. """ with pytest.raises(CertificateError): DNSPattern.from_bytes(b"www.google.com\0nasty.h4x0r.com") def test_catches_ip_address(self): """ IP addresses are invalid and raise a :class:`CertificateError`. """ with pytest.raises(CertificateError): DNSPattern.from_bytes(b"192.168.0.0") def test_invalid_wildcard(self): """ Integration test with _validate_pattern: catches double wildcards thus is used if an wildward is present. """ with pytest.raises(CertificateError): DNSPattern.from_bytes(b"*.foo.*") class TestURIPattern: def test_enforces_bytes(self): """ Raise TypeError if unicode is passed. """ with pytest.raises(TypeError): URIPattern.from_bytes("sip:foo.com") def test_catches_missing_colon(self): """ Raise CertificateError if URI doesn't contain a `:`. """ with pytest.raises(CertificateError): URIPattern.from_bytes(b"sip;foo.com") def test_catches_wildcards(self): """ Raise CertificateError if URI contains a *. """ with pytest.raises(CertificateError): URIPattern.from_bytes(b"sip:*.foo.com") class TestSRVPattern: def test_enforces_bytes(self): """ Raise TypeError if unicode is passed. """ with pytest.raises(TypeError): SRVPattern.from_bytes("_mail.example.com") def test_catches_missing_underscore(self): """ Raise CertificateError if SRV doesn't start with a `_`. """ with pytest.raises(CertificateError): SRVPattern.from_bytes(b"foo.com") def test_catches_wildcards(self): """ Raise CertificateError if SRV contains a *. """ with pytest.raises(CertificateError): SRVPattern.from_bytes(b"sip:*.foo.com") class TestValidateDNSWildcardPattern: def test_allows_only_one_wildcard(self): """ Raise CertificateError on multiple wildcards. """ with pytest.raises(CertificateError): _validate_pattern(b"*.*.com") def test_wildcard_must_be_left_most(self): """ Raise CertificateError if wildcard is not in the left-most part. """ for hn in [b"foo.b*r.com", b"foo.bar.c*m", b"foo.*", b"foo.*.com"]: with pytest.raises(CertificateError): _validate_pattern(hn) def test_must_have_at_least_three_parts(self): """ Raise CertificateError if host consists of less than three parts. """ for hn in [ b"*", b"*.com", b"*fail.com", b"*foo", b"foo*", b"f*o", b"*.example.", ]: with pytest.raises(CertificateError): _validate_pattern(hn) def test_valid_patterns(self): """ Does not throw CertificateError on valid patterns. """ for pattern in [ b"*.bar.com", b"*oo.bar.com", b"f*.bar.com", b"f*o.bar.com", ]: _validate_pattern(pattern) class TestIPAddressPattern: def test_invalid_ip(self): """ Raises CertificateError on invalid IP addresses. """ with pytest.raises(CertificateError): IPAddressPattern.from_bytes(b"127.o.o.1") @pytest.mark.parametrize("ip_s", ["1.1.1.1", "::1"]) def test_verify_equal(self, ip_s): """ Return True if IP addresses are identical. """ ip = ipaddress.ip_address(ip_s) assert IPAddress_ID(ip).verify(IPAddressPattern(ip)) is True class FakeCertID: pass class Fake_ID: """ An ID that accepts exactly on object as pattern. """ def __init__(self, pattern): self._pattern = pattern def verify(self, other): """ True iff other is the same object as pattern. """ return other is self._pattern class TestFindMatches: def test_one_match(self): """ If there's a match, return a tuple of the certificate id and the service id. """ valid_cert_id = FakeCertID() valid_id = Fake_ID(valid_cert_id) rv = _find_matches( [FakeCertID(), valid_cert_id, FakeCertID()], [valid_id] ) assert [ ServiceMatch(cert_pattern=valid_cert_id, service_id=valid_id) ] == rv def test_no_match(self): """ If no valid certificate ids are found, return an empty list. """ rv = _find_matches( [FakeCertID(), FakeCertID(), FakeCertID()], [Fake_ID(object())] ) assert [] == rv def test_multiple_matches(self): """ Return all matches. """ valid_cert_id_1 = FakeCertID() valid_cert_id_2 = FakeCertID() valid_cert_id_3 = FakeCertID() valid_id_1 = Fake_ID(valid_cert_id_1) valid_id_2 = Fake_ID(valid_cert_id_2) valid_id_3 = Fake_ID(valid_cert_id_3) rv = _find_matches( [ FakeCertID(), valid_cert_id_1, FakeCertID(), valid_cert_id_3, FakeCertID(), valid_cert_id_2, ], [valid_id_1, valid_id_2, valid_id_3], ) assert [ ServiceMatch(cert_pattern=valid_cert_id_1, service_id=valid_id_1), ServiceMatch(cert_pattern=valid_cert_id_2, service_id=valid_id_2), ServiceMatch(cert_pattern=valid_cert_id_3, service_id=valid_id_3), ] == rv class TestIsIPAddress: @pytest.mark.parametrize( "ip", [ b"127.0.0.1", "127.0.0.1", "172.16.254.12", "*.0.0.1", "::1", "*::1", "2001:0db8:0000:0000:0000:ff00:0042:8329", "2001:0db8::ff00:0042:8329", ], ) def test_ips(self, ip): """ Returns True for patterns and hosts that could match IP addresses. """ assert _is_ip_address(ip) is True @pytest.mark.parametrize( "not_ip", [ b"*.twistedmatrix.com", b"twistedmatrix.com", b"mail.google.com", b"omega7.de", b"omega7", b"127.\xff.0.1", ], ) def test_not_ips(self, not_ip): """ Return False for patterns and hosts that aren't IP addresses. """ assert _is_ip_address(not_ip) is False class TestVerificationError: def test_repr_str(self): """ The __str__ and __repr__ methods return something helpful. """ with pytest.raises(VerificationError) as ei: raise VerificationError(errors=["foo"]) assert repr(ei.value) == str(ei.value) assert str(ei.value) != "" @pytest.mark.parametrize("proto", range(pickle.HIGHEST_PROTOCOL + 1)) @pytest.mark.parametrize( "exc", [ VerificationError(errors=[]), VerificationError(errors=[DNSMismatch("example.com")]), VerificationError([]), VerificationError([DNSMismatch("example.com")]), ], ) def test_pickle(self, exc, proto): """ Exceptions can be pickled and unpickled. """ new_exc = pickle.loads(pickle.dumps(exc, proto)) # Exceptions can't be compared. assert exc.__class__ == new_exc.__class__ assert exc.__dict__ == new_exc.__dict__ service_identity-24.2.0/tests/test_packaging.py0000644000000000000000000000120214707112756016602 0ustar00from importlib import metadata import pytest import service_identity class TestLegacyMetadataHack: def test_version(self): """ service_identity.__version__ returns the correct version. """ assert ( metadata.version("service-identity") == service_identity.__version__ ) def test_does_not_exist(self): """ Asking for unsupported dunders raises an AttributeError. """ with pytest.raises( AttributeError, match="module service_identity has no attribute __yolo__", ): service_identity.__yolo__ service_identity-24.2.0/tests/test_pyopenssl.py0000644000000000000000000001112014707112756016712 0ustar00import ipaddress import pytest from service_identity.exceptions import ( DNSMismatch, IPAddressMismatch, VerificationError, ) from service_identity.hazmat import ( DNS_ID, DNSPattern, IPAddress_ID, IPAddressPattern, URIPattern, ) from service_identity.pyopenssl import ( extract_ids, extract_patterns, verify_hostname, verify_ip_address, ) from .certificates import ( PEM_CN_ONLY, PEM_DNS_ONLY, PEM_EVERYTHING, PEM_OTHER_NAME, ) if pytest.importorskip("OpenSSL"): from OpenSSL.crypto import FILETYPE_PEM, load_certificate CERT_DNS_ONLY = load_certificate(FILETYPE_PEM, PEM_DNS_ONLY) CERT_CN_ONLY = load_certificate(FILETYPE_PEM, PEM_CN_ONLY) CERT_OTHER_NAME = load_certificate(FILETYPE_PEM, PEM_OTHER_NAME) CERT_EVERYTHING = load_certificate(FILETYPE_PEM, PEM_EVERYTHING) class TestPublicAPI: def test_verify_hostname_ok(self): """ verify_hostname succeeds if the hostnames match. """ class FakeConnection: def get_peer_certificate(self): return CERT_DNS_ONLY verify_hostname(FakeConnection(), "twistedmatrix.com") def test_verify_hostname_fail(self): """ verify_hostname fails if the hostnames don't match and provides the user with helpful information. """ class FakeConnection: def get_peer_certificate(self): return CERT_DNS_ONLY with pytest.raises(VerificationError) as ei: verify_hostname(FakeConnection(), "google.com") assert [ DNSMismatch(mismatched_id=DNS_ID("google.com")) ] == ei.value.errors @pytest.mark.parametrize("ip", ["1.1.1.1", "::1"]) def test_verify_ip_address_ok(self, ip): """ verify_ip_address succeeds if the addresses match. Works both with IPv4 and IPv6. """ class FakeConnection: def get_peer_certificate(self): return CERT_EVERYTHING verify_ip_address(FakeConnection(), ip) @pytest.mark.parametrize("ip", ["1.1.1.2", "::2"]) def test_verify_ip_address_fail(self, ip): """ verify_ip_address fails if the addresses don't match and provides the user with helpful information. Works both with IPv4 and IPv6. """ class FakeConnection: def get_peer_certificate(self): return CERT_EVERYTHING with pytest.raises(VerificationError) as ei: verify_ip_address(FakeConnection(), ip) assert [ IPAddressMismatch(mismatched_id=IPAddress_ID(ip)) ] == ei.value.errors class TestExtractPatterns: def test_dns(self): """ Returns the correct DNSPattern from a certificate. """ rv = extract_patterns(CERT_DNS_ONLY) assert [ DNSPattern.from_bytes(b"www.twistedmatrix.com"), DNSPattern.from_bytes(b"twistedmatrix.com"), ] == rv def test_cn_ids_are_ignored(self): """ commonName is not supported anymore and therefore ignored. """ assert [] == extract_patterns(CERT_CN_ONLY) def test_uri(self): """ Returns the correct URIPattern from a certificate. """ rv = extract_patterns(CERT_OTHER_NAME) assert [URIPattern.from_bytes(b"http://example.com/")] == [ id for id in rv if isinstance(id, URIPattern) ] def test_ip(self): """ Returns IP patterns. """ rv = extract_patterns(CERT_EVERYTHING) assert [ DNSPattern.from_bytes(pattern=b"service.identity.invalid"), DNSPattern.from_bytes( pattern=b"*.wildcard.service.identity.invalid" ), DNSPattern.from_bytes(pattern=b"service.identity.invalid"), DNSPattern.from_bytes(pattern=b"single.service.identity.invalid"), IPAddressPattern(pattern=ipaddress.IPv4Address("1.1.1.1")), IPAddressPattern(pattern=ipaddress.IPv6Address("::1")), IPAddressPattern(pattern=ipaddress.IPv4Address("2.2.2.2")), IPAddressPattern(pattern=ipaddress.IPv6Address("2a00:1c38::53")), ] == rv def test_extract_ids_deprecated(self): """ `extract_ids` raises a DeprecationWarning with correct stacklevel. """ with pytest.deprecated_call() as wr: extract_ids(CERT_EVERYTHING) w = wr.pop() assert ( "`extract_ids()` is deprecated, please use `extract_patterns()`." == w.message.args[0] ) assert __file__ == w.filename service_identity-24.2.0/tests/constraints/oldest-cryptography.txt0000644000000000000000000000003514707112756022403 0ustar00attrs==19.1.0 cryptography<3 service_identity-24.2.0/tests/constraints/oldest-pyopenssl.txt0000644000000000000000000000006014707112756021702 0ustar00attrs==19.1.0 cryptography<35 pyOpenSSL==17.1.0 service_identity-24.2.0/tests/typing/api.py0000644000000000000000000000222214707112756015705 0ustar00""" This module is used to test the typing of the public API of service-identity. It is NOT intended to be executed. """ from __future__ import annotations import socket from typing import Sequence from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_pem_x509_certificate from OpenSSL import SSL import service_identity backend = default_backend() c_cert = load_pem_x509_certificate("foo.pem", backend) c_ids: Sequence[service_identity.hazmat.CertificatePattern] = ( service_identity.cryptography.extract_patterns(c_cert) ) service_identity.cryptography.verify_certificate_hostname( c_cert, "example.com" ) service_identity.cryptography.verify_certificate_ip_address( c_cert, "127.0.0.1" ) ctx = SSL.Context(SSL.TLSv1_2_METHOD) conn = SSL.Connection(ctx, socket.socket(socket.AF_INET, socket.SOCK_STREAM)) p_cert = conn.get_peer_certificate() assert p_cert p_ids: Sequence[service_identity.hazmat.CertificatePattern] = ( service_identity.pyopenssl.extract_patterns(p_cert) ) service_identity.pyopenssl.verify_hostname(conn, "example.com") service_identity.pyopenssl.verify_ip_address(conn, "127.0.0.1") service_identity-24.2.0/.gitignore0000644000000000000000000000014214707112756014075 0ustar00.tox dist build .coverage* .venv htmlcov _build *.pyc .cache .pytest_cache .vscode .envrc .direnv service_identity-24.2.0/LICENSE0000644000000000000000000000214014707112756013112 0ustar00The MIT License (MIT) Copyright (c) 2014 Hynek Schlawack and the service-identity contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. service_identity-24.2.0/pyproject.toml0000644000000000000000000001277514707112756015040 0ustar00[build-system] requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [project] name = "service-identity" authors = [{ name = "Hynek Schlawack", email = "hs@ox.cx" }] license = "MIT" requires-python = ">=3.8" description = "Service identity verification for pyOpenSSL & cryptography." keywords = ["cryptography", "openssl", "pyopenssl"] classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Security :: Cryptography", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] dependencies = [ # Keep in-sync with tests/constraints/*. "attrs>=19.1.0", "pyasn1-modules", "pyasn1", "cryptography", ] dynamic = ["version", "readme"] [project.optional-dependencies] idna = ["idna"] tests = ["coverage[toml]>=5.0.2", "pytest"] docs = ["sphinx", "furo", "myst-parser", "sphinx-notfound-page", "pyOpenSSL"] mypy = ["mypy", "types-pyOpenSSL", "idna"] dev = ["service-identity[tests,mypy,idna]", "pyOpenSSL"] [project.urls] Documentation = "https://service-identity.readthedocs.io/" Changelog = "https://service-identity.readthedocs.io/en/stable/changelog.html" GitHub = "https://github.com/pyca/service-identity" Funding = "https://github.com/sponsors/hynek" Tidelift = "https://tidelift.com/subscription/pkg/pypi-service-identity?utm_source=pypi-service-identity&utm_medium=pypi" Mastodon = "https://mastodon.social/@hynek" Twitter = "https://twitter.com/hynek" [tool.hatch.version] source = "vcs" raw-options = { local_scheme = "no-local-version" } [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] text = "# Service Identity Verification for pyOpenSSL & *cryptography*\n" [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] path = "README.md" start-after = "spiel-begin -->\n" [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] text = """ ## Release Information """ [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] path = "CHANGELOG.md" pattern = "\n(###.+?\n)## " [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] text = """ ---- [Complete Changelog →](https://service-identity.readthedocs.io/en/stable/changelog.html) """ [tool.pytest.ini_options] addopts = ["-ra", "--strict-markers", "--strict-config"] xfail_strict = true testpaths = "tests" filterwarnings = ["once::Warning"] norecursedirs = ["tests/typing"] [tool.coverage.run] parallel = true branch = true source = ["service_identity"] [tool.coverage.paths] source = ["src", ".tox/py*/**/site-packages"] [tool.coverage.report] show_missing = true skip_covered = true exclude_lines = [ # a more strict default pragma "\\# pragma: no cover\\b", # allow defensive code "^\\s*raise AssertionError\\b", "^\\s*raise NotImplementedError\\b", "^\\s*return NotImplemented\\b", "^\\s*raise$", # typing-related code "^if (False|TYPE_CHECKING):", ": \\.\\.\\.(\\s*#.*)?$", "^ +\\.\\.\\.$", "-> ['\"]?NoReturn['\"]?:", ] [tool.interrogate] omit-covered-files = true verbose = 2 fail-under = 100 whitelist-regex = ["test_.*"] [tool.ruff] src = ["src", "tests"] line-length = 79 [tool.ruff.lint] select = ["ALL"] ignore = [ "A001", # shadowing is fine "ANN", # Mypy is better at this "ARG001", # we don't control all args passed in "ARG005", # we need stub lambdas "COM", # ruff format takes care of our commas "D", # We prefer our own docstring style. "E501", # leave line-length enforcement to ruff format "FIX", # Yes, we want XXX as a marker. "INP001", # sometimes we want Python files outside of packages "ISC001", # conflicts with ruff format "N801", # some artistic freedom when naming things after RFCs "N802", # ditto "PLR2004", # numbers are sometimes fine "RUF001", # leave my smart characters alone "SLF001", # private members are accessed by friendly functions "TCH", # TYPE_CHECKING blocks break autodocs "TD", # we don't follow other people's todo style ] [tool.ruff.lint.per-file-ignores] "tests/*" = [ "B018", # "useless" expressions can be useful in tests "PLC1901", # empty strings are falsey, but are less specific in tests "PT005", # we always add underscores and explicit name "PT011", # broad is fine "S101", # assert "S301", # I know pickle is bad, but people use it. "SIM300", # Yoda rocks in asserts "TRY301", # tests need to raise exceptions ] "docs/pyopenssl_example.py" = [ "T201", # print is fine in the example "T203", # pprint is fine in the example ] [tool.ruff.lint.isort] lines-between-types = 1 lines-after-imports = 2 [tool.mypy] strict = true pretty = true show_error_codes = true enable_error_code = ["ignore-without-code"] ignore_missing_imports = true [[tool.mypy.overrides]] module = "tests.*" ignore_errors = true [[tool.mypy.overrides]] module = "tests.typing.*" ignore_errors = false [[tool.mypy.overrides]] module = "cryptography.*" follow_imports = "skip" service_identity-24.2.0/PKG-INFO0000644000000000000000000001203014707112756013201 0ustar00Metadata-Version: 2.3 Name: service-identity Version: 24.2.0 Summary: Service identity verification for pyOpenSSL & cryptography. Project-URL: Documentation, https://service-identity.readthedocs.io/ Project-URL: Changelog, https://service-identity.readthedocs.io/en/stable/changelog.html Project-URL: GitHub, https://github.com/pyca/service-identity Project-URL: Funding, https://github.com/sponsors/hynek Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-service-identity?utm_source=pypi-service-identity&utm_medium=pypi Project-URL: Mastodon, https://mastodon.social/@hynek Project-URL: Twitter, https://twitter.com/hynek Author-email: Hynek Schlawack License-Expression: MIT License-File: LICENSE Keywords: cryptography,openssl,pyopenssl Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Security :: Cryptography Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Typing :: Typed Requires-Python: >=3.8 Requires-Dist: attrs>=19.1.0 Requires-Dist: cryptography Requires-Dist: pyasn1 Requires-Dist: pyasn1-modules Provides-Extra: dev Requires-Dist: coverage[toml]>=5.0.2; extra == 'dev' Requires-Dist: idna; extra == 'dev' Requires-Dist: mypy; extra == 'dev' Requires-Dist: pyopenssl; extra == 'dev' Requires-Dist: pytest; extra == 'dev' Requires-Dist: types-pyopenssl; extra == 'dev' Provides-Extra: docs Requires-Dist: furo; extra == 'docs' Requires-Dist: myst-parser; extra == 'docs' Requires-Dist: pyopenssl; extra == 'docs' Requires-Dist: sphinx; extra == 'docs' Requires-Dist: sphinx-notfound-page; extra == 'docs' Provides-Extra: idna Requires-Dist: idna; extra == 'idna' Provides-Extra: mypy Requires-Dist: idna; extra == 'mypy' Requires-Dist: mypy; extra == 'mypy' Requires-Dist: types-pyopenssl; extra == 'mypy' Provides-Extra: tests Requires-Dist: coverage[toml]>=5.0.2; extra == 'tests' Requires-Dist: pytest; extra == 'tests' Description-Content-Type: text/markdown # Service Identity Verification for pyOpenSSL & *cryptography* Use this package if: - you want to **verify** that a [PyCA *cryptography*](https://cryptography.io/) certificate is valid for a certain hostname or IP address, - or if you use [pyOpenSSL](https://pypi.org/project/pyOpenSSL/) and don’t want to be [**MITM**](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)ed, - or if you want to **inspect** certificates from either for service IDs. *service-identity* aspires to give you all the tools you need for verifying whether a certificate is valid for the intended purposes. In the simplest case, this means *host name verification*. However, *service-identity* implements [RFC 6125](https://datatracker.ietf.org/doc/html/rfc6125.html) fully. Also check out [*pem*](https://github.com/hynek/pem) that makes loading certificates from all kinds of PEM-encoded files a breeze! ## Project Information *service-identity* is released under the [MIT](https://github.com/pyca/service-identity/blob/main/LICENSE) license, its documentation lives at [Read the Docs](https://service-identity.readthedocs.io/), the code on [GitHub](https://github.com/pyca/service-identity), and the latest release on [PyPI](https://pypi.org/project/service-identity/). ### Credits *service-identity* is written and maintained by [Hynek Schlawack](https://hynek.me/). The development is kindly supported by my employer [Variomedia AG](https://www.variomedia.de/), *service-identity*'s [Tidelift subscribers](https://tidelift.com/lifter/search/pypi/service-identity), and all my amazing [GitHub Sponsors](https://github.com/sponsors/hynek). ### *service-identity* for Enterprise Available as part of the [Tidelift Subscription](https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek). The maintainers of *service-identity* and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open-source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. ## Release Information ### Added - Python 3.13 is now officially supported. [#74](https://github.com/pyca/service-identity/pull/74) ### Changed - pyOpenSSL's identity extraction has been reimplemented using *cryptography*'s primitives instead of deprecated pyOpenSSL APIs. As a result, the oldest supported pyOpenSSL version is now 17.1.0. [#70](https://github.com/pyca/service-identity/pull/70) ---- [Complete Changelog →](https://service-identity.readthedocs.io/en/stable/changelog.html)