Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints #57

Merged
merged 10 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ jobs:
strategy:
matrix:
python-version:
- "3.7"
- "3.8"
- "3.9"
- "3.10"
Expand All @@ -56,7 +55,7 @@ jobs:
allow-prereleases: true
cache: pip

- name: Install & run tox
- name: Prepare tox & run tests
run: |
V=${{ matrix.python-version }}

Expand All @@ -69,6 +68,9 @@ jobs:
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@v3
with:
Expand Down Expand Up @@ -111,6 +113,21 @@ jobs:
path: htmlcov
if: ${{ failure() }}

mypy-pkg:
name: Type-check package
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
cache: pip

- name: Install & run tox
run: |
python -Im pip install tox
python -Im tox run -e mypy-pkg

install-dev:
strategy:
matrix:
Expand Down Expand Up @@ -155,6 +172,7 @@ jobs:
- docs
- install-dev
- lint
- mypy-pkg

runs-on: ubuntu-latest

Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ If breaking changes are needed do be done, they are:

### Backwards-incompatible Changes

- All Python versions up to and including 3.6 have been dropped.
- 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.
Expand All @@ -33,6 +33,8 @@ If breaking changes are needed do be done, they are:
- `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 (2021-05-09)
Expand Down
6 changes: 5 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ API

.. note::

So far, public APIs are only available for host names (:rfc:`6125`) and IP addresses (:rfc:`2818`).
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 <https://github.com/pyca/service-identity/blob/main/src/service_identity/hazmat.py>`_.

Expand Down Expand Up @@ -54,6 +54,10 @@ 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
Expand Down
3 changes: 3 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"deflist",
]

# Move type hints into the description block, instead of the func definition.
autodoc_typehints = "description"
autodoc_typehints_description_target = "documented"

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
Expand Down
11 changes: 4 additions & 7 deletions docs/pyopenssl_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
hostname = sys.argv[1]

ctx = SSL.Context(SSL.TLSv1_2_METHOD)
ctx.set_verify(SSL.VERIFY_PEER, lambda conn, cert, errno, depth, ok: ok)
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))
Expand All @@ -22,12 +22,9 @@
try:
conn.do_handshake()

print("Server certificate is valid for the following patterns:\n")
pprint.pprint(
service_identity.pyopenssl.extract_patterns(
conn.get_peer_certificate()
)
)
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)
Expand Down
46 changes: 41 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ build-backend = "hatchling.build"
name = "service-identity"
authors = [{ name = "Hynek Schlawack", email = "hs@ox.cx" }]
license = "MIT"
requires-python = ">=3.7"
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.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand All @@ -23,22 +22,23 @@ classifiers = [
"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",
"importlib_metadata;python_version<'3.8'",
]
dynamic = ["version", "readme"]

[project.optional-dependencies]
idna = ["idna"]
tests = ["coverage[toml]>=5.0.2", "pytest"]
docs = ["sphinx", "furo", "myst-parser", "sphinx-notfound-page"]
dev = ["service-identity[tests,docs,idna]", "pyOpenSSL"]
docs = ["sphinx", "furo", "myst-parser", "sphinx-notfound-page", "pyOpenSSL"]
mypy = ["mypy", "types-pyOpenSSL", "idna"]
dev = ["service-identity[tests,mypy,docs,idna]", "pyOpenSSL"]

[project.urls]
Documentation = "https://service-identity.readthedocs.io/"
Expand Down Expand Up @@ -105,6 +105,22 @@ 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.black]
Expand Down Expand Up @@ -152,3 +168,23 @@ ignore = [
[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"
6 changes: 1 addition & 5 deletions src/service_identity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,9 @@ def __getattr__(name: str) -> str:
if name not in dunder_to_metadata.keys():
raise AttributeError(f"module {__name__} has no attribute {name}")

import sys
import warnings

if sys.version_info < (3, 8):
from importlib_metadata import metadata
else:
from importlib.metadata import metadata
from importlib.metadata import metadata

warnings.warn(
f"Accessing service_identity.{name} is deprecated and will be "
Expand Down
2 changes: 1 addition & 1 deletion src/service_identity/cryptography.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def extract_patterns(cert: Certificate) -> Sequence[CertificatePattern]:
srv, _ = decode(other.value)
if isinstance(srv, IA5String):
ids.append(SRVPattern.from_bytes(srv.asOctets()))
else: # pragma: nocover
else: # pragma: no cover
raise CertificateError("Unexpected certificate content.")

return ids
Expand Down
50 changes: 25 additions & 25 deletions src/service_identity/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
them from __init__.py.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Sequence


if TYPE_CHECKING:
from .hazmat import ServiceID

import attr

Expand All @@ -23,52 +30,45 @@ class SubjectAltNameWarning(DeprecationWarning):
"""


@attr.s(auto_exc=True)
class VerificationError(Exception):
"""
Service identity verification failed.
"""

errors = attr.ib()

def __str__(self):
return self.__repr__()
@attr.s(slots=True)
class Mismatch:
mismatched_id: ServiceID = attr.ib()


@attr.s
class DNSMismatch:
class DNSMismatch(Mismatch):
"""
No matching DNSPattern could be found.
"""

mismatched_id = attr.ib()


@attr.s
class SRVMismatch:
class SRVMismatch(Mismatch):
"""
No matching SRVPattern could be found.
"""

mismatched_id = attr.ib()


@attr.s
class URIMismatch:
class URIMismatch(Mismatch):
"""
No matching URIPattern could be found.
"""

mismatched_id = attr.ib()


@attr.s
class IPAddressMismatch:
class IPAddressMismatch(Mismatch):
"""
No matching IPAddressPattern could be found.
"""

mismatched_id = attr.ib()

@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):
Expand Down
Loading