Skip to content

Commit

Permalink
Add type hints (#57)
Browse files Browse the repository at this point in the history
* Add type hints

* Fix ci

* Install pyOpenSSL for docs for intersphinx

* Clarify

* Zap stray setting

* Fix ServiceID protocol

Co-authored-by: Tin Tvrtković <[email protected]>

* Clarify

* Add typing example

* Only check API types across versions

* Clarify

---------

Co-authored-by: Tin Tvrtković <[email protected]>
  • Loading branch information
hynek and Tinche authored Jun 13, 2023
1 parent 7578496 commit 0578c9a
Show file tree
Hide file tree
Showing 14 changed files with 225 additions and 103 deletions.
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 = "[email protected]" }]
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

0 comments on commit 0578c9a

Please sign in to comment.