service_identity-24.1.0/.git_archival.txt 0000644 0000000 0000000 00000000175 14550703657 015367 0 ustar 00 node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
ref-names: $Format:%D$
service_identity-24.1.0/.gitattributes 0000644 0000000 0000000 00000000217 14550703657 015004 0 ustar 00 # 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.1.0/.pre-commit-config.yaml 0000644 0000000 0000000 00000001333 14550703657 016372 0 ustar 00 ---
repos:
- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.13
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
args: [-L, fo]
- repo: https://github.com/econchick/interrogate
rev: 1.5.0
hooks:
- id: interrogate
language_version: python3.11
args: [tests, -v]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-toml
- id: check-yaml
service_identity-24.1.0/.python-version-default 0000644 0000000 0000000 00000000005 14550703657 016533 0 ustar 00 3.12
service_identity-24.1.0/.readthedocs.yaml 0000644 0000000 0000000 00000000330 14550703657 015334 0 ustar 00 ---
version: 2
build:
os: ubuntu-22.04
tools:
# Keep in-sync with tox.ini/docs and ci.yml/docs
python: "3.12"
python:
install:
- method: pip
path: .
extra_requirements:
- docs
service_identity-24.1.0/CHANGELOG.md 0000644 0000000 0000000 00000016170 14550703657 013727 0 ustar 00 # 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.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.1.0/README.md 0000644 0000000 0000000 00000006033 14550703657 013372 0 ustar 00 # Service Identity Verification
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.
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.
[Learn more.](https://tidelift.com/lifter/search/pypi/service-identity)
service_identity-24.1.0/tox.ini 0000644 0000000 0000000 00000003517 14550703657 013432 0 ustar 00 [tox]
min_version = 4
env_list =
lint,
mypy-{api,pkg},
docs,
pypy3{,-pyopenssl-latest-idna},
py3{8,9,10,11,12}{,-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
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.1.0/.github/CODE_OF_CONDUCT.md 0000644 0000000 0000000 00000006137 14550703657 016257 0 ustar 00 # 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.1.0/.github/CONTRIBUTING.md 0000644 0000000 0000000 00000021254 14550703657 015706 0 ustar 00 # 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 use [Ruff](https://ruff.rs/) to sort our imports, and we use [Black](https://github.com/psf/black) with line length of 79 characters to format our code.
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.1.0/.github/FUNDING.yml 0000644 0000000 0000000 00000000064 14550703657 015266 0 ustar 00 ---
github: hynek
tidelift: "pypi/service-identity"
service_identity-24.1.0/.github/SECURITY.md 0000644 0000000 0000000 00000001372 14550703657 015245 0 ustar 00 # Security Policy
## Supported Versions
We are following [*CalVer*](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.1.0/.github/dependabot.yml 0000644 0000000 0000000 00000000173 14550703657 016302 0 ustar 00 ---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
service_identity-24.1.0/.github/workflows/ci.yml 0000644 0000000 0000000 00000010477 14550703657 016635 0 ustar 00 ---
name: CI
on:
push:
branches: [main]
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: "3.11" # XXX: change once interrogate works on 3.12
cache: pip
- name: Install & run tox
run: |
python -Im pip install tox
python -Im tox run -e lint -- --show-diff-on-failure
tests:
name: Tests on ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "pypy-3.8"
- "pypy-3.9"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip
- name: Prepare tox & run tests
run: |
V=${{ matrix.python-version }}
if [[ "$V" = pypy-* ]]; then
V=pypy3
else
V=py$(echo $V | tr -d .)
fi
python -Im pip install tox
python -Im tox run -f "$V"
- name: Run Mypy on API
run: python -Im tox run -e mypy-api
- name: Upload coverage data
uses: actions/upload-artifact@v4
with:
name: coverage-data-${{ matrix.python-version }}
path: .coverage.*
if-no-files-found: ignore
coverage:
name: Combine & check coverage
needs: tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version-file: .python-version-default
cache: pip
- uses: actions/download-artifact@v4
with:
pattern: coverage-data-*
merge-multiple: true
- name: Combine coverage & fail if it's <100%
run: |
python -Im pip install coverage[toml]
python -Im coverage combine
python -Im coverage html --skip-covered --skip-empty
# Report and write to summary.
python -Im coverage report --format=markdown >> $GITHUB_STEP_SUMMARY
# Report again and fail if under 100%.
python -Im 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: Type-check package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version-file: .python-version-default
cache: pip
- name: Install & run tox
run: |
python -Im pip install tox
python -Im 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__)'
docs:
name: Build docs & run doctests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
# Keep in sync with tox.ini/docs & .readthedocs.yaml
python-version: "3.12"
cache: pip
- name: Install & run tox
run: |
python -Im pip install tox
python -Im tox run -e docs
# Ensure everything required is passing for branch protection.
required-checks-pass:
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.1.0/.github/workflows/codeql-analysis.yml 0000644 0000000 0000000 00000001365 14550703657 021326 0 ustar 00 ---
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.1.0/.github/workflows/pypi-package.yml 0000644 0000000 0000000 00000003132 14550703657 020602 0 ustar 00 ---
name: Build & maybe upload PyPI package
on:
push:
branches: [main]
tags: ["*"]
pull_request:
release:
types:
- published
workflow_dispatch:
permissions:
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
# 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.1.0/docs/Makefile 0000644 0000000 0000000 00000015222 14550703657 014503 0 ustar 00 # 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.1.0/docs/api.rst 0000644 0000000 0000000 00000003476 14550703657 014356 0 ustar 00 ===
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.1.0/docs/changelog.md 0000644 0000000 0000000 00000000041 14550703657 015305 0 ustar 00 ```{include} ../CHANGELOG.md
```
service_identity-24.1.0/docs/conf.py 0000644 0000000 0000000 00000014135 14550703657 014344 0 ustar 00 from importlib import metadata
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.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 encoding of source files.
# source_encoding = 'utf-8-sig'
# 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"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
# pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# -- Options for HTML output ----------------------------------------------
html_theme = "furo"
html_theme_options = {
"top_of_page_button": None,
"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",
)
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- 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,
)
]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- 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",
)
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
# texinfo_no_detailmenu = False
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.1.0/docs/implemented-standards.md 0000644 0000000 0000000 00000000265 14550703657 017652 0 ustar 00 # 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.1.0/docs/index.md 0000644 0000000 0000000 00000001552 14550703657 014475 0 ustar 00 # 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.1.0/docs/installation.md 0000644 0000000 0000000 00000002511 14550703657 016063 0 ustar 00 # 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.1.0/docs/license.md 0000644 0000000 0000000 00000000427 14550703657 015010 0 ustar 00 # 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.1.0/docs/pyopenssl_example.py 0000644 0000000 0000000 00000002240 14550703657 017160 0 ustar 00 import 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.1.0/docs/_static/custom.css 0000644 0000000 0000000 00000000425 14550703657 016514 0 ustar 00 @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.1.0/src/service_identity/__init__.py 0000644 0000000 0000000 00000002554 14550703657 020370 0 ustar 00 """
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:
dunder_to_metadata = {
"__version__": "version",
"__description__": "summary",
"__uri__": "",
"__url__": "",
"__email__": "",
}
if name not in dunder_to_metadata:
raise AttributeError(f"module {__name__} has no attribute {name}")
import warnings
from importlib.metadata import metadata
warnings.warn(
f"Accessing service_identity.{name} is deprecated and will be "
"removed in a future release. Use importlib.metadata directly "
"to query packaging metadata.",
DeprecationWarning,
stacklevel=2,
)
meta = metadata("service-identity")
if name in ("__uri__", "__url__"):
return meta["Project-URL"].split(" ", 1)[1]
if name == "__email__":
return meta["Author-email"].split("<", 1)[1].rstrip(">")
return meta[dunder_to_metadata[name]]
service_identity-24.1.0/src/service_identity/cryptography.py 0000644 0000000 0000000 00000012177 14550703657 021366 0 ustar 00 """
`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
raise CertificateError("Unexpected certificate content.")
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.1.0/src/service_identity/exceptions.py 0000644 0000000 0000000 00000002753 14550703657 021013 0 ustar 00 """
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.1.0/src/service_identity/hazmat.py 0000644 0000000 0000000 00000030170 14550703657 020110 0 ustar 00 """
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:
raise CertificateError(
"Certificate does not contain any `subjectAltName`s."
)
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(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(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(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)
return True
except ValueError:
pass
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):
raise TypeError("The DNS pattern must be a bytes string.")
pattern = pattern.strip()
if pattern == b"" or _is_ip_address(pattern) or b"\0" in pattern:
raise CertificateError(f"Invalid DNS pattern {pattern!r}.")
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:
raise CertificateError(
f"Invalid IP address pattern {bs!r}."
) 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):
raise TypeError("The URI pattern must be a bytes string.")
pattern = pattern.strip().translate(_TRANS_TO_LOWER)
if b":" not in pattern or b"*" in pattern or _is_ip_address(pattern):
raise CertificateError(f"Invalid URI pattern {pattern!r}.")
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):
raise TypeError("The SRV pattern must be a bytes string.")
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)
):
raise CertificateError(f"Invalid SRV pattern {pattern!r}.")
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):
raise TypeError("DNS-ID must be a text string.")
hostname = hostname.strip()
if not hostname or _is_ip_address(hostname):
raise ValueError("Invalid DNS-ID.")
if any(ord(c) > 127 for c in hostname):
if idna:
ascii_id = idna.encode(hostname)
else:
raise ImportError(
"idna library is required for non-ASCII IDs."
)
else:
ascii_id = hostname.encode("ascii")
self.hostname = ascii_id.translate(_TRANS_TO_LOWER)
if self._RE_LEGAL_CHARS.match(self.hostname) is None:
raise ValueError("Invalid DNS-ID.")
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):
raise TypeError("URI-ID must be a text string.")
uri = uri.strip()
if ":" not in uri or _is_ip_address(uri):
raise ValueError("Invalid URI-ID.")
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):
raise TypeError("SRV-ID must be a text string.")
srv = srv.strip()
if "." not in srv or _is_ip_address(srv) or srv[0] != "_":
raise ValueError("Invalid SRV-ID.")
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 == b"*" or cert_head == 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:
raise CertificateError(
f"Certificate's DNS-ID {cert_pattern!r} contains too many wildcards."
)
parts = cert_pattern.split(b".")
if len(parts) < 3:
raise CertificateError(
f"Certificate's DNS-ID {cert_pattern!r} has too few host components for "
"wildcard usage."
)
# We assume there will always be only one wildcard allowed.
if b"*" not in parts[0]:
raise CertificateError(
"Certificate's DNS-ID {!r} has a wildcard outside the left-most "
"part.".format(cert_pattern)
)
if any(not len(p) for p in parts):
raise CertificateError(
f"Certificate's DNS-ID {cert_pattern!r} contains empty parts."
)
# Ensure no locale magic interferes.
_TRANS_TO_LOWER = bytes.maketrans(
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", b"abcdefghijklmnopqrstuvwxyz"
)
service_identity-24.1.0/src/service_identity/py.typed 0000644 0000000 0000000 00000000000 14550703657 017736 0 ustar 00 service_identity-24.1.0/src/service_identity/pyopenssl.py 0000644 0000000 0000000 00000012715 14550703657 020665 0 ustar 00 """
`pyOpenSSL `_-specific code.
"""
from __future__ import annotations
import contextlib
import warnings
from typing import Sequence
from pyasn1.codec.der.decoder import decode
from pyasn1.type.char import IA5String
from pyasn1.type.univ import ObjectIdentifier
from pyasn1_modules.rfc2459 import GeneralNames
from .exceptions import CertificateError
from .hazmat import (
DNS_ID,
CertificatePattern,
DNSPattern,
IPAddress_ID,
IPAddressPattern,
SRVPattern,
URIPattern,
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=[],
)
ID_ON_DNS_SRV = ObjectIdentifier("1.3.6.1.5.5.7.8.7") # id_on_dnsSRV
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.
"""
ids: list[CertificatePattern] = []
for i in range(cert.get_extension_count()):
ext = cert.get_extension(i)
if ext.get_short_name() == b"subjectAltName":
names, _ = decode(ext.get_data(), asn1Spec=GeneralNames())
for n in names:
name_string = n.getName()
if name_string == "dNSName":
ids.append(
DNSPattern.from_bytes(n.getComponent().asOctets())
)
elif name_string == "iPAddress":
ids.append(
IPAddressPattern.from_bytes(
n.getComponent().asOctets()
)
)
elif name_string == "uniformResourceIdentifier":
ids.append(
URIPattern.from_bytes(n.getComponent().asOctets())
)
elif name_string == "otherName":
comp = n.getComponent()
oid = comp.getComponentByPosition(0)
if oid == ID_ON_DNS_SRV:
srv, _ = decode(comp.getComponentByPosition(1))
if isinstance(srv, IA5String):
ids.append(SRVPattern.from_bytes(srv.asOctets()))
else: # pragma: no cover
raise CertificateError(
"Unexpected certificate content."
)
else: # pragma: no cover
pass
else: # pragma: no cover
pass
return ids
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.1.0/tests/__init__.py 0000644 0000000 0000000 00000000000 14550703657 015352 0 ustar 00 service_identity-24.1.0/tests/certificates.py 0000644 0000000 0000000 00000017067 14550703657 016305 0 ustar 00 from 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.1.0/tests/conftest.py 0000644 0000000 0000000 00000001033 14550703657 015447 0 ustar 00 import 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.1.0/tests/test_cryptography.py 0000644 0000000 0000000 00000012212 14550703657 017415 0 ustar 00 import 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.1.0/tests/test_hazmat.py 0000644 0000000 0000000 00000051621 14550703657 016155 0 ustar 00 import 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.
"""
try:
raise VerificationError(errors=["foo"])
except VerificationError as e:
assert repr(e) == str(e)
assert str(e) != ""
@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.1.0/tests/test_packaging.py 0000644 0000000 0000000 00000003106 14550703657 016610 0 ustar 00 from importlib import metadata
import pytest
import service_identity
class TestLegacyMetadataHack:
def test_version(self):
"""
service_identity.__version__ returns the correct version.
"""
with pytest.deprecated_call():
assert (
metadata.version("service-identity")
== service_identity.__version__
)
def test_description(self):
"""
service_identity.__description__ returns the correct description.
"""
with pytest.deprecated_call():
assert (
"Service identity verification for pyOpenSSL & cryptography."
== service_identity.__description__
)
@pytest.mark.parametrize("name", ["uri", "url"])
def test_uri(self, name):
"""
service_identity.__uri__ & __url__ return the correct project URL.
"""
with pytest.deprecated_call():
assert "https://service-identity.readthedocs.io/" == getattr(
service_identity, f"__{name}__"
)
def test_email(self):
"""
service_identity.__email__ returns Hynek's email address.
"""
with pytest.deprecated_call():
assert "hs@ox.cx" == service_identity.__email__
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.1.0/tests/test_pyopenssl.py 0000644 0000000 0000000 00000011120 14550703657 016713 0 ustar 00 import 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.1.0/tests/constraints/oldest-cryptography.txt 0000644 0000000 0000000 00000000035 14550703657 022404 0 ustar 00 attrs==19.1.0
cryptography<3
service_identity-24.1.0/tests/constraints/oldest-pyopenssl.txt 0000644 0000000 0000000 00000000060 14550703657 021703 0 ustar 00 attrs==19.1.0
cryptography<35
pyOpenSSL==17.0.0
service_identity-24.1.0/tests/typing/api.py 0000644 0000000 0000000 00000002216 14550703657 015711 0 ustar 00 """
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.1.0/.gitignore 0000644 0000000 0000000 00000000142 14550703657 014076 0 ustar 00 .tox
dist
build
.coverage*
.venv
htmlcov
_build
*.pyc
.cache
.pytest_cache
.vscode
.envrc
.direnv
service_identity-24.1.0/LICENSE 0000644 0000000 0000000 00000002140 14550703657 013113 0 ustar 00 The 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.1.0/pyproject.toml 0000644 0000000 0000000 00000012046 14550703657 015030 0 ustar 00 [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 :: 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",
"ignore:::aiohttp[.*]",
"ignore:::importlib[.*]",
"ignore::DeprecationWarning:twisted.python.threadable",
]
[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.black]
line-length = 79
[tool.ruff]
src = ["src", "tests"]
select = [
"E", # pycodestyle
"W", # pycodestyle
"F", # Pyflakes
"UP", # pyupgrade
"N", # pep8-naming
"YTT", # flake8-2020
"S", # flake8-bandit
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"T10", # flake8-debugger
"ISC", # flake8-implicit-str-concat
"RET", # flake8-return
"SIM", # flake8-simplify
"DTZ", # flake8-datetimez
"I", # isort
"PGH", # pygrep-hooks
"PLC", # Pylint
"PIE", # flake8-pie
"RUF", # ruff
]
ignore = [
"RUF001", # leave my smart characters alone
"N801", # some artistic freedom when naming things after RFCs
"N802", # ditto
]
[tool.ruff.per-file-ignores]
"tests/*" = [
"S101", # assert
"S301", # I know pickle is bad, but people use it.
"SIM300", # Yoda rocks in tests
"PLC1901", # empty strings are falsey, but are less specific in tests
"B018", # "useless" expressions can be useful in tests
]
[tool.ruff.isort]
lines-between-types = 1
lines-after-imports = 2
[tool.mypy]
strict = 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.1.0/PKG-INFO 0000644 0000000 0000000 00000011322 14550703657 013205 0 ustar 00 Metadata-Version: 2.1
Name: service-identity
Version: 24.1.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 :: 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: pyopenssl; extra == 'dev'
Requires-Dist: service-identity[idna,mypy,tests]; 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.
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.
[Learn more.](https://tidelift.com/lifter/search/pypi/service-identity)
## Release Information
### 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)
----
[Complete Changelog →](https://service-identity.readthedocs.io/en/stable/changelog.html)