From dd106e42ede25b4e82bb8ee754a0870807e716f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 08:57:41 +0100 Subject: [PATCH 1/9] Bump the dependencies group with 6 updates (#640) --- docs-requirements.txt | 4 ++-- lint-requirements.txt | 6 +++--- test-requirements.txt | 8 +++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index b1154be..fa81080 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -8,7 +8,7 @@ alabaster==0.7.16 # via sphinx babel==2.14.0 # via sphinx -certifi==2023.11.17 +certifi==2024.2.2 # via requests cffi==1.16.0 # via cryptography @@ -54,5 +54,5 @@ sphinxcontrib-serializinghtml==1.1.10 # via sphinx sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in -urllib3==2.2.0 +urllib3==2.2.1 # via requests diff --git a/lint-requirements.txt b/lint-requirements.txt index aa9362b..fb6a22b 100644 --- a/lint-requirements.txt +++ b/lint-requirements.txt @@ -24,11 +24,11 @@ pluggy==1.4.0 # via pytest pycparser==2.21 # via cffi -pytest==8.0.0 +pytest==8.0.2 # via -r lint-requirements.in toml==0.10.2 # via mypy -types-pyopenssl==24.0.0.20240130 +types-pyopenssl==24.0.0.20240228 # via -r lint-requirements.in -typing-extensions==4.9.0 +typing-extensions==4.10.0 # via mypy diff --git a/test-requirements.txt b/test-requirements.txt index 48899f7..cca3e89 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,8 +8,10 @@ attrs==23.2.0 # via service-identity cffi==1.16.0 # via cryptography -coverage[toml]==7.4.1 - # via -r test-requirements.in +coverage[toml]==7.4.3 + # via + # -r test-requirements.in + # coverage cryptography==41.0.7 # via # -r test-requirements.in @@ -33,7 +35,7 @@ pycparser==2.21 # via cffi pyopenssl==24.0.0 # via -r test-requirements.in -pytest==8.0.0 +pytest==8.0.2 # via -r test-requirements.in service-identity==24.1.0 # via -r test-requirements.in From b3a767f336e20600f30c9ff78385a58352ff6ee3 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 27 Mar 2024 16:32:24 +0200 Subject: [PATCH 2/9] Add AKI to child CA certificates (#642) --- newsfragments/642.bugfix.rst | 1 + src/trustme/__init__.py | 15 ++++++++++++--- tests/test_trustme.py | 5 +++++ 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 newsfragments/642.bugfix.rst diff --git a/newsfragments/642.bugfix.rst b/newsfragments/642.bugfix.rst new file mode 100644 index 0000000..9d75e7a --- /dev/null +++ b/newsfragments/642.bugfix.rst @@ -0,0 +1 @@ +Add the Authority Key Identifier extension to child CA certificates. diff --git a/src/trustme/__init__.py b/src/trustme/__init__.py index 5fb24fb..d126180 100644 --- a/src/trustme/__init__.py +++ b/src/trustme/__init__.py @@ -246,18 +246,27 @@ def __init__( ) issuer = name sign_key = self._private_key + aki: Optional[x509.AuthorityKeyIdentifier] if parent_cert is not None: sign_key = parent_cert._private_key parent_certificate = parent_cert._certificate issuer = parent_certificate.subject - - self._certificate = ( + ski_ext = parent_certificate.extensions.get_extension_for_class( + x509.SubjectKeyIdentifier) + aki = x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ski_ext.value) + else: + aki = None + cert_builder = ( _cert_builder_common(name, issuer, self._private_key.public_key()) .add_extension( x509.BasicConstraints(ca=True, path_length=path_length), critical=True, ) - .add_extension( + ) + if aki: + cert_builder = cert_builder.add_extension(aki, critical=False) + self._certificate = ( + cert_builder.add_extension( x509.KeyUsage( digital_signature=True, # OCSP content_commitment=False, diff --git a/tests/test_trustme.py b/tests/test_trustme.py index 1d901ad..581716e 100644 --- a/tests/test_trustme.py +++ b/tests/test_trustme.py @@ -200,6 +200,11 @@ def test_intermediate() -> None: assert_is_ca(child_ca_cert) assert child_ca_cert.issuer == ca_cert.subject assert _path_length(child_ca_cert) == 8 + aki = child_ca_cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) + assert aki.critical is False + expected_aki_key_id = ca_cert.extensions.get_extension_for_class( + x509.SubjectKeyIdentifier).value.digest + assert aki.value.key_identifier == expected_aki_key_id child_server = child_ca.issue_cert("test-host.example.org") assert len(child_server.cert_chain_pems) == 2 From 6e7af247ccafa65d4d10212aa1ca0ebc862771c4 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Fri, 24 May 2024 08:06:01 +0400 Subject: [PATCH 3/9] Format code with black and isort (#639) * Format code with black and isort * Restore cryptography 41 --- lint-requirements.in | 2 + lint-requirements.txt | 22 +++++-- lint.sh | 3 +- src/trustme/__init__.py | 133 +++++++++++++++++++++------------------- src/trustme/_cli.py | 19 ++++-- tests/test_cli.py | 18 ++++-- tests/test_trustme.py | 124 +++++++++++++++++-------------------- 7 files changed, 174 insertions(+), 147 deletions(-) diff --git a/lint-requirements.in b/lint-requirements.in index 0d7337d..cefb21f 100644 --- a/lint-requirements.in +++ b/lint-requirements.in @@ -3,3 +3,5 @@ cryptography>=35.0.0 types-pyopenssl>=20.0.4 pytest>=6.2 idna>=3.2 +black +isort diff --git a/lint-requirements.txt b/lint-requirements.txt index fb6a22b..dce6cda 100644 --- a/lint-requirements.txt +++ b/lint-requirements.txt @@ -1,25 +1,39 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile lint-requirements.in # +black==24.2.0 + # via -r lint-requirements.in cffi==1.16.0 # via cryptography +click==8.1.7 + # via black cryptography==41.0.7 # via # -r lint-requirements.in # types-pyopenssl -idna==3.4 +idna==3.6 # via -r lint-requirements.in iniconfig==2.0.0 # via pytest +isort==5.13.2 + # via -r lint-requirements.in mypy==0.910 # via -r lint-requirements.in mypy-extensions==0.4.4 - # via mypy + # via + # black + # mypy packaging==23.2 - # via pytest + # via + # black + # pytest +pathspec==0.12.1 + # via black +platformdirs==4.2.0 + # via black pluggy==1.4.0 # via pytest pycparser==2.21 diff --git a/lint.sh b/lint.sh index 112902c..2b7f6a0 100755 --- a/lint.sh +++ b/lint.sh @@ -12,5 +12,6 @@ python -m pip --version python -m pip install -Ur lint-requirements.txt # Linting - +black --check src/trustme tests +isort --profile black src/trustme tests mypy src/trustme tests diff --git a/src/trustme/__init__.py b/src/trustme/__init__.py index d126180..98dee02 100644 --- a/src/trustme/__init__.py +++ b/src/trustme/__init__.py @@ -1,30 +1,32 @@ from __future__ import annotations + import datetime import ipaddress import os import ssl -from enum import Enum from base64 import urlsafe_b64encode from contextlib import contextmanager +from enum import Enum from tempfile import NamedTemporaryFile -from typing import Generator, List, Optional, Union, TYPE_CHECKING +from typing import TYPE_CHECKING, Generator, List, Optional, Union import idna - from cryptography import x509 from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import rsa, ec +from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.hazmat.primitives.serialization import ( - PrivateFormat, NoEncryption + Encoding, + NoEncryption, + PrivateFormat, + load_pem_private_key, ) from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID -from cryptography.hazmat.primitives.serialization import Encoding -from cryptography.hazmat.primitives.serialization import load_pem_private_key from ._version import __version__ if TYPE_CHECKING: # pragma: no cover import OpenSSL.SSL + CERTIFICATE_PUBLIC_KEY_TYPES = Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey] CERTIFICATE_PRIVATE_KEY_TYPES = Union[rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey] @@ -38,7 +40,11 @@ DEFAULT_NOT_BEFORE = datetime.datetime(2000, 1, 1) -def _name(name: str, organization_name: Optional[str] = None, common_name: Optional[str] = None) -> x509.Name: +def _name( + name: str, + organization_name: Optional[str] = None, + common_name: Optional[str] = None, +) -> x509.Name: name_pieces = [ x509.NameAttribute( NameOID.ORGANIZATION_NAME, @@ -47,9 +53,7 @@ def _name(name: str, organization_name: Optional[str] = None, common_name: Optio x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, name), ] if common_name is not None: - name_pieces.append( - x509.NameAttribute(NameOID.COMMON_NAME, common_name) - ) + name_pieces.append(x509.NameAttribute(NameOID.COMMON_NAME, common_name)) return x509.Name(name_pieces) @@ -138,16 +142,17 @@ class Blob: `CA.cert_pem` or `LeafCert.private_key_and_cert_chain_pem`. """ + def __init__(self, data: bytes) -> None: self._data = data def bytes(self) -> bytes: - """Returns the data as a `bytes` object. - - """ + """Returns the data as a `bytes` object.""" return self._data - def write_to_path(self, path: Union[str, "os.PathLike[str]"], append: bool = False) -> None: + def write_to_path( + self, path: Union[str, "os.PathLike[str]"], append: bool = False + ) -> None: """Writes the data to the file at the given path. Args: @@ -215,9 +220,7 @@ def _generate_key(self) -> CERTIFICATE_PRIVATE_KEY_TYPES: # key_size needs to be a least 2048 to be accepted # on Debian and pressumably other OSes - return rsa.generate_private_key( - public_exponent=65537, key_size=2048 - ) + return rsa.generate_private_key(public_exponent=65537, key_size=2048) elif self is KeyType.ECDSA: return ec.generate_private_key(ec.SECP256R1()) else: # pragma: no cover @@ -226,6 +229,7 @@ def _generate_key(self) -> CERTIFICATE_PRIVATE_KEY_TYPES: class CA: """A certificate authority.""" + _certificate: x509.Certificate def __init__( @@ -276,8 +280,9 @@ def __init__( key_cert_sign=True, # sign certs crl_sign=True, # sign revocation lists encipher_only=False, - decipher_only=False), - critical=True + decipher_only=False, + ), + critical=True, ) .sign( private_key=sign_key, @@ -297,11 +302,9 @@ def private_key_pem(self) -> Blob: other certificates from this CA.""" return Blob( self._private_key.private_bytes( - Encoding.PEM, - PrivateFormat.TraditionalOpenSSL, - NoEncryption() - ) + Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption() ) + ) def create_child_ca(self, key_type: KeyType = KeyType.ECDSA) -> "CA": """Creates a child certificate authority @@ -378,15 +381,16 @@ def issue_cert( """ if not identities and common_name is None: - raise ValueError( - "Must specify at least one identity or common name" - ) + raise ValueError("Must specify at least one identity or common name") key = key_type._generate_key() ski_ext = self._certificate.extensions.get_extension_for_class( - x509.SubjectKeyIdentifier) - aki = x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ski_ext.value) + x509.SubjectKeyIdentifier + ) + aki = x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( + ski_ext.value + ) cert = ( _cert_builder_common( @@ -421,16 +425,19 @@ def issue_cert( key_cert_sign=False, crl_sign=False, encipher_only=False, - decipher_only=False), - critical=True + decipher_only=False, + ), + critical=True, ) .add_extension( - x509.ExtendedKeyUsage([ - ExtendedKeyUsageOID.CLIENT_AUTH, - ExtendedKeyUsageOID.SERVER_AUTH, - ExtendedKeyUsageOID.CODE_SIGNING, - ]), - critical=True + x509.ExtendedKeyUsage( + [ + ExtendedKeyUsageOID.CLIENT_AUTH, + ExtendedKeyUsageOID.SERVER_AUTH, + ExtendedKeyUsageOID.CODE_SIGNING, + ] + ), + critical=True, ) .sign( private_key=self._private_key, @@ -445,14 +452,14 @@ def issue_cert( ca = ca.parent_cert return LeafCert( - key.private_bytes( - Encoding.PEM, - PrivateFormat.TraditionalOpenSSL, - NoEncryption(), - ), - cert.public_bytes(Encoding.PEM), - chain_to_ca, - ) + key.private_bytes( + Encoding.PEM, + PrivateFormat.TraditionalOpenSSL, + NoEncryption(), + ), + cert.public_bytes(Encoding.PEM), + chain_to_ca, + ) # For backwards compatibility issue_server_cert = issue_cert @@ -466,18 +473,17 @@ def configure_trust(self, ctx: Union[ssl.SSLContext, OpenSSL.SSL.Context]) -> No """ if isinstance(ctx, ssl.SSLContext): - ctx.load_verify_locations( - cadata=self.cert_pem.bytes().decode("ascii")) + ctx.load_verify_locations(cadata=self.cert_pem.bytes().decode("ascii")) elif _smells_like_pyopenssl(ctx): from OpenSSL import crypto - cert = crypto.load_certificate( - crypto.FILETYPE_PEM, self.cert_pem.bytes()) + + cert = crypto.load_certificate(crypto.FILETYPE_PEM, self.cert_pem.bytes()) store = ctx.get_cert_store() store.add_cert(cert) else: raise TypeError( - "unrecognized context type {!r}" - .format(ctx.__class__.__name__)) + "unrecognized context type {!r}".format(ctx.__class__.__name__) + ) @classmethod def from_pem(cls, cert_bytes: bytes, private_key_bytes: bytes) -> "CA": @@ -517,12 +523,15 @@ class LeafCert: cert chain. """ - def __init__(self, private_key_pem: bytes, server_cert_pem: bytes, chain_to_ca: List[bytes]) -> None: + + def __init__( + self, private_key_pem: bytes, server_cert_pem: bytes, chain_to_ca: List[bytes] + ) -> None: self.private_key_pem = Blob(private_key_pem) - self.cert_chain_pems = [ - Blob(pem) for pem in [server_cert_pem] + chain_to_ca] - self.private_key_and_cert_chain_pem = ( - Blob(private_key_pem + server_cert_pem + b''.join(chain_to_ca))) + self.cert_chain_pems = [Blob(pem) for pem in [server_cert_pem] + chain_to_ca] + self.private_key_and_cert_chain_pem = Blob( + private_key_pem + server_cert_pem + b"".join(chain_to_ca) + ) def configure_cert(self, ctx: Union[ssl.SSLContext, OpenSSL.SSL.Context]) -> None: """Configure the given context object to present this certificate. @@ -537,18 +546,16 @@ def configure_cert(self, ctx: Union[ssl.SSLContext, OpenSSL.SSL.Context]) -> Non with self.private_key_and_cert_chain_pem.tempfile() as path: ctx.load_cert_chain(path) elif _smells_like_pyopenssl(ctx): - from OpenSSL.crypto import ( - load_privatekey, load_certificate, FILETYPE_PEM, - ) + from OpenSSL.crypto import FILETYPE_PEM, load_certificate, load_privatekey + key = load_privatekey(FILETYPE_PEM, self.private_key_pem.bytes()) ctx.use_privatekey(key) - cert = load_certificate(FILETYPE_PEM, - self.cert_chain_pems[0].bytes()) + cert = load_certificate(FILETYPE_PEM, self.cert_chain_pems[0].bytes()) ctx.use_certificate(cert) for pem in self.cert_chain_pems[1:]: cert = load_certificate(FILETYPE_PEM, pem.bytes()) ctx.add_extra_chain_cert(cert) else: raise TypeError( - "unrecognized context type {!r}" - .format(ctx.__class__.__name__)) + "unrecognized context type {!r}".format(ctx.__class__.__name__) + ) diff --git a/src/trustme/_cli.py b/src/trustme/_cli.py index f32aa90..3e73673 100644 --- a/src/trustme/_cli.py +++ b/src/trustme/_cli.py @@ -1,13 +1,14 @@ import argparse import os -import trustme import sys -from typing import List, Optional from datetime import datetime +from typing import List, Optional +import trustme # ISO 8601 -DATE_FORMAT = '%Y-%m-%d' +DATE_FORMAT = "%Y-%m-%d" + def main(argv: Optional[List[str]] = None) -> None: if argv is None: @@ -38,7 +39,7 @@ def main(argv: Optional[List[str]] = None) -> None: "--expires-on", default=None, help="Set the date the certificate will expire on (in YYYY-MM-DD format).", - metavar='YYYY-MM-DD', + metavar="YYYY-MM-DD", ) parser.add_argument( "-q", @@ -57,7 +58,11 @@ def main(argv: Optional[List[str]] = None) -> None: cert_dir = args.dir identities = [str(identity) for identity in args.identities] common_name = str(args.common_name[0]) if args.common_name else None - expires_on = None if args.expires_on is None else datetime.strptime(args.expires_on, DATE_FORMAT) + expires_on = ( + None + if args.expires_on is None + else datetime.strptime(args.expires_on, DATE_FORMAT) + ) quiet = args.quiet key_type = trustme.KeyType[args.key_type] @@ -68,7 +73,9 @@ def main(argv: Optional[List[str]] = None) -> None: # Generate the CA certificate ca = trustme.CA(key_type=key_type) - cert = ca.issue_cert(*identities, common_name=common_name, not_after=expires_on, key_type=key_type) + cert = ca.issue_cert( + *identities, common_name=common_name, not_after=expires_on, key_type=key_type + ) # Write the certificate and private key the server should use server_key = os.path.join(cert_dir, "server.key") diff --git a/tests/test_cli.py b/tests/test_cli.py index ac0e477..a71b7d9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,6 @@ +import os import subprocess import sys -import os from pathlib import Path import pytest @@ -45,7 +45,9 @@ def test_trustme_cli_directory_does_not_exist(tmp_path: Path) -> None: main(argv=["-d", str(notdir)]) -def test_trustme_cli_identities(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_trustme_cli_identities( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.chdir(tmp_path) main(argv=["-i", "example.org", "www.example.org"]) @@ -60,7 +62,9 @@ def test_trustme_cli_identities_empty(tmp_path: Path) -> None: main(argv=["-i"]) -def test_trustme_cli_common_name(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_trustme_cli_common_name( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.chdir(tmp_path) main(argv=["--common-name", "localhost"]) @@ -70,7 +74,9 @@ def test_trustme_cli_common_name(tmp_path: Path, monkeypatch: pytest.MonkeyPatch assert tmp_path.joinpath("client.pem").exists() -def test_trustme_cli_expires_on(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_trustme_cli_expires_on( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.chdir(tmp_path) main(argv=["--expires-on", "2035-03-01"]) @@ -80,7 +86,9 @@ def test_trustme_cli_expires_on(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) assert tmp_path.joinpath("client.pem").exists() -def test_trustme_cli_invalid_expires_on(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_trustme_cli_invalid_expires_on( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.chdir(tmp_path) with pytest.raises(ValueError, match="does not match format"): diff --git a/tests/test_trustme.py b/tests/test_trustme.py index 581716e..97199a3 100644 --- a/tests/test_trustme.py +++ b/tests/test_trustme.py @@ -1,24 +1,24 @@ -import pytest - -import sys -import ssl -import socket import datetime +import socket +import ssl +import sys from concurrent.futures import ThreadPoolExecutor -from ipaddress import IPv4Address, IPv6Address, IPv4Network, IPv6Network +from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network from pathlib import Path from typing import Callable, Optional, Union -from cryptography import x509 -from cryptography.hazmat.primitives.serialization import ( - Encoding, PublicFormat, load_pem_private_key) - import OpenSSL.SSL +import pytest import service_identity.pyopenssl # type: ignore[import] +from cryptography import x509 +from cryptography.hazmat.primitives.serialization import ( + Encoding, + PublicFormat, + load_pem_private_key, +) import trustme -from trustme import CA, LeafCert, KeyType - +from trustme import CA, KeyType, LeafCert SslSocket = Union[ssl.SSLSocket, OpenSSL.SSL.Connection] @@ -55,11 +55,13 @@ def assert_is_leaf(leaf_cert: x509.Certificate) -> None: assert ku.critical is True eku = leaf_cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage) - assert eku.value == x509.ExtendedKeyUsage([ - x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH, - x509.oid.ExtendedKeyUsageOID.SERVER_AUTH, - x509.oid.ExtendedKeyUsageOID.CODE_SIGNING - ]) + assert eku.value == x509.ExtendedKeyUsage( + [ + x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH, + x509.oid.ExtendedKeyUsageOID.SERVER_AUTH, + x509.oid.ExtendedKeyUsageOID.CODE_SIGNING, + ] + ) assert eku.critical is True @@ -102,7 +104,9 @@ def test_basics(key_type: KeyType, expected_key_header: bytes) -> None: assert b"PRIVATE KEY" in server.private_key_pem.bytes() assert b"BEGIN CERTIFICATE" in server.cert_chain_pems[0].bytes() assert len(server.cert_chain_pems) == 1 - assert server.private_key_pem.bytes() in server.private_key_and_cert_chain_pem.bytes() + assert ( + server.private_key_pem.bytes() in server.private_key_and_cert_chain_pem.bytes() + ) for blob in server.cert_chain_pems: assert blob.bytes() in server.private_key_and_cert_chain_pem.bytes() @@ -119,38 +123,32 @@ def test_basics(key_type: KeyType, expected_key_header: bytes) -> None: def test_ca_custom_names() -> None: ca = CA( - organization_name='python-trio', - organization_unit_name='trustme', + organization_name="python-trio", + organization_unit_name="trustme", ) ca_cert = x509.load_pem_x509_certificate(ca.cert_pem.bytes()) assert { - 'O=python-trio', - 'OU=trustme', - }.issubset({ - rdn.rfc4514_string() - for rdn in ca_cert.subject.rdns - }) + "O=python-trio", + "OU=trustme", + }.issubset({rdn.rfc4514_string() for rdn in ca_cert.subject.rdns}) def test_issue_cert_custom_names() -> None: ca = CA() leaf_cert = ca.issue_cert( - 'example.org', - organization_name='python-trio', - organization_unit_name='trustme', + "example.org", + organization_name="python-trio", + organization_unit_name="trustme", ) cert = x509.load_pem_x509_certificate(leaf_cert.cert_chain_pems[0].bytes()) assert { - 'O=python-trio', - 'OU=trustme', - }.issubset({ - rdn.rfc4514_string() - for rdn in cert.subject.rdns - }) + "O=python-trio", + "OU=trustme", + }.issubset({rdn.rfc4514_string() for rdn in cert.subject.rdns}) def test_issue_cert_custom_not_after() -> None: @@ -277,6 +275,7 @@ def test_blob(tmp_path: Path) -> None: with open(path, "rb") as f: assert f.read() == test_data + def test_ca_from_pem(tmp_path: Path) -> None: ca1 = trustme.CA() ca2 = trustme.CA.from_pem(ca1.cert_pem.bytes(), ca1.private_key_pem.bytes()) @@ -359,11 +358,12 @@ def doit(ca: CA, hostname: str, server_cert: LeafCert) -> None: @pytest.mark.parametrize("key_type", [KeyType.RSA, KeyType.ECDSA]) def test_stdlib_end_to_end(key_type: KeyType) -> None: - def wrap_client(ca: CA, raw_client_sock: socket.socket, hostname: str) -> ssl.SSLSocket: + def wrap_client( + ca: CA, raw_client_sock: socket.socket, hostname: str + ) -> ssl.SSLSocket: ctx = ssl.create_default_context() ca.configure_trust(ctx) - wrapped_client_sock = ctx.wrap_socket( - raw_client_sock, server_hostname=hostname) + wrapped_client_sock = ctx.wrap_socket(raw_client_sock, server_hostname=hostname) print("Client got server cert:", wrapped_client_sock.getpeercert()) peercert = wrapped_client_sock.getpeercert() assert peercert is not None @@ -371,11 +371,12 @@ def wrap_client(ca: CA, raw_client_sock: socket.socket, hostname: str) -> ssl.SS assert san == (("DNS", "my-test-host.example.org"),) return wrapped_client_sock - def wrap_server(server_cert: LeafCert, raw_server_sock: socket.socket) -> ssl.SSLSocket: + def wrap_server( + server_cert: LeafCert, raw_server_sock: socket.socket + ) -> ssl.SSLSocket: ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) server_cert.configure_cert(ctx) - wrapped_server_sock = ctx.wrap_socket( - raw_server_sock, server_side=True) + wrapped_server_sock = ctx.wrap_socket(raw_server_sock, server_side=True) print("server encrypted with:", wrapped_server_sock.cipher()) return wrapped_server_sock @@ -384,12 +385,15 @@ def wrap_server(server_cert: LeafCert, raw_server_sock: socket.socket) -> ssl.SS @pytest.mark.parametrize("key_type", [KeyType.RSA, KeyType.ECDSA]) def test_pyopenssl_end_to_end(key_type: KeyType) -> None: - def wrap_client(ca: CA, raw_client_sock: socket.socket, hostname: str) -> OpenSSL.SSL.Connection: + def wrap_client( + ca: CA, raw_client_sock: socket.socket, hostname: str + ) -> OpenSSL.SSL.Connection: # Cribbed from example at # https://service-identity.readthedocs.io/en/stable/api.html#service_identity.pyopenssl.verify_hostname ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) - ctx.set_verify(OpenSSL.SSL.VERIFY_PEER, - lambda conn, cert, errno, depth, ok: bool(ok)) + ctx.set_verify( + OpenSSL.SSL.VERIFY_PEER, lambda conn, cert, errno, depth, ok: bool(ok) + ) ca.configure_trust(ctx) conn = OpenSSL.SSL.Connection(ctx, raw_client_sock) conn.set_connect_state() @@ -397,7 +401,9 @@ def wrap_client(ca: CA, raw_client_sock: socket.socket, hostname: str) -> OpenSS service_identity.pyopenssl.verify_hostname(conn, hostname) return conn - def wrap_server(server_cert: LeafCert, raw_server_sock: socket.socket) -> OpenSSL.SSL.Connection: + def wrap_server( + server_cert: LeafCert, raw_server_sock: socket.socket + ) -> OpenSSL.SSL.Connection: ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) server_cert.configure_cert(ctx) @@ -419,38 +425,30 @@ def test_identity_variants() -> None: cases = { # Traditional ascii hostname "example.org": x509.DNSName("example.org"), - # Wildcard "*.example.org": x509.DNSName("*.example.org"), - # IDN "éxamplë.org": x509.DNSName("xn--xampl-9rat.org"), "xn--xampl-9rat.org": x509.DNSName("xn--xampl-9rat.org"), - # IDN + wildcard "*.éxamplë.org": x509.DNSName("*.xn--xampl-9rat.org"), "*.xn--xampl-9rat.org": x509.DNSName("*.xn--xampl-9rat.org"), - # IDN that acts differently in IDNA-2003 vs IDNA-2008 "faß.de": x509.DNSName("xn--fa-hia.de"), "xn--fa-hia.de": x509.DNSName("xn--fa-hia.de"), - # IDN with non-permissable character (uppercase K) # (example taken from idna package docs) "Königsgäßchen.de": x509.DNSName("xn--knigsgchen-b4a3dun.de"), - # IP addresses "127.0.0.1": x509.IPAddress(IPv4Address("127.0.0.1")), "::1": x509.IPAddress(IPv6Address("::1")), # Check normalization "0000::1": x509.IPAddress(IPv6Address("::1")), - # IP networks "127.0.0.0/24": x509.IPAddress(IPv4Network("127.0.0.0/24")), "2001::/16": x509.IPAddress(IPv6Network("2001::/16")), # Check normalization "2001:0000::/16": x509.IPAddress(IPv6Network("2001::/16")), - # Email address "example@example.com": x509.RFC822Name("example@example.com"), } @@ -462,9 +460,7 @@ def test_identity_variants() -> None: print(f"testing: {hostname!r}") pem = ca.issue_cert(hostname).cert_chain_pems[0].bytes() cert = x509.load_pem_x509_certificate(pem) - san = cert.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) + san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) assert_is_leaf(cert) got = list(san.value)[0] assert got == expected @@ -486,27 +482,19 @@ def test_CN() -> None: # Default is no common name pem = ca.issue_cert("example.com").cert_chain_pems[0].bytes() cert = x509.load_pem_x509_certificate(pem) - common_names = cert.subject.get_attributes_for_oid( - x509.oid.NameOID.COMMON_NAME - ) + common_names = cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME) assert common_names == [] # Common name on its own is valid pem = ca.issue_cert(common_name="woo").cert_chain_pems[0].bytes() cert = x509.load_pem_x509_certificate(pem) - common_names = cert.subject.get_attributes_for_oid( - x509.oid.NameOID.COMMON_NAME - ) + common_names = cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME) assert common_names[0].value == "woo" # Common name + SAN pem = ca.issue_cert("example.com", common_name="woo").cert_chain_pems[0].bytes() cert = x509.load_pem_x509_certificate(pem) - san = cert.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ) + san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) assert list(san.value)[0] == x509.DNSName("example.com") - common_names = cert.subject.get_attributes_for_oid( - x509.oid.NameOID.COMMON_NAME - ) + common_names = cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME) assert common_names[0].value == "woo" From 68144e13ec6642fd6efe9087816992c195c5166f Mon Sep 17 00:00:00 2001 From: EXPLOSION Date: Fri, 24 May 2024 13:36:32 +0900 Subject: [PATCH 4/9] Run black (#647) --- src/trustme/__init__.py | 53 ++++++++++++++++++++--------------------- tests/test_trustme.py | 3 ++- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/trustme/__init__.py b/src/trustme/__init__.py index 98dee02..3996535 100644 --- a/src/trustme/__init__.py +++ b/src/trustme/__init__.py @@ -256,38 +256,37 @@ def __init__( parent_certificate = parent_cert._certificate issuer = parent_certificate.subject ski_ext = parent_certificate.extensions.get_extension_for_class( - x509.SubjectKeyIdentifier) - aki = x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ski_ext.value) + x509.SubjectKeyIdentifier + ) + aki = x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( + ski_ext.value + ) else: aki = None - cert_builder = ( - _cert_builder_common(name, issuer, self._private_key.public_key()) - .add_extension( - x509.BasicConstraints(ca=True, path_length=path_length), - critical=True, - ) + cert_builder = _cert_builder_common( + name, issuer, self._private_key.public_key() + ).add_extension( + x509.BasicConstraints(ca=True, path_length=path_length), + critical=True, ) if aki: cert_builder = cert_builder.add_extension(aki, critical=False) - self._certificate = ( - cert_builder.add_extension( - x509.KeyUsage( - digital_signature=True, # OCSP - content_commitment=False, - key_encipherment=False, - data_encipherment=False, - key_agreement=False, - key_cert_sign=True, # sign certs - crl_sign=True, # sign revocation lists - encipher_only=False, - decipher_only=False, - ), - critical=True, - ) - .sign( - private_key=sign_key, - algorithm=hashes.SHA256(), - ) + self._certificate = cert_builder.add_extension( + x509.KeyUsage( + digital_signature=True, # OCSP + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, # sign certs + crl_sign=True, # sign revocation lists + encipher_only=False, + decipher_only=False, + ), + critical=True, + ).sign( + private_key=sign_key, + algorithm=hashes.SHA256(), ) @property diff --git a/tests/test_trustme.py b/tests/test_trustme.py index 97199a3..eee0a39 100644 --- a/tests/test_trustme.py +++ b/tests/test_trustme.py @@ -201,7 +201,8 @@ def test_intermediate() -> None: aki = child_ca_cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) assert aki.critical is False expected_aki_key_id = ca_cert.extensions.get_extension_for_class( - x509.SubjectKeyIdentifier).value.digest + x509.SubjectKeyIdentifier + ).value.digest assert aki.value.key_identifier == expected_aki_key_id child_server = child_ca.issue_cert("test-host.example.org") From 8eda84f861071e9c433ee402dd5cedc2de55563f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:58:23 +0000 Subject: [PATCH 5/9] Bump urllib3 from 2.2.1 to 2.2.2 Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index fa81080..3afffbf 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -54,5 +54,5 @@ sphinxcontrib-serializinghtml==1.1.10 # via sphinx sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in -urllib3==2.2.1 +urllib3==2.2.2 # via requests From faea96e0ac835439eb3f905951d7b6940cbad449 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Jul 2024 11:34:03 +0400 Subject: [PATCH 6/9] Bump certifi from 2024.2.2 to 2024.7.4 (#654) --- docs-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index 3afffbf..8246ce2 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -8,7 +8,7 @@ alabaster==0.7.16 # via sphinx babel==2.14.0 # via sphinx -certifi==2024.2.2 +certifi==2024.7.4 # via requests cffi==1.16.0 # via cryptography From 519be6a83395310eb6e7cc9b9b42c7797c657633 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Sat, 6 Jul 2024 22:27:15 +0400 Subject: [PATCH 7/9] Update mypy to 1.10.1 --- lint-requirements.in | 2 +- lint-requirements.txt | 6 +++--- src/trustme/__init__.py | 2 +- tests/test_trustme.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lint-requirements.in b/lint-requirements.in index cefb21f..9a38e30 100644 --- a/lint-requirements.in +++ b/lint-requirements.in @@ -1,4 +1,4 @@ -mypy==0.910 +mypy cryptography>=35.0.0 types-pyopenssl>=20.0.4 pytest>=6.2 diff --git a/lint-requirements.txt b/lint-requirements.txt index dce6cda..c7c3dfb 100644 --- a/lint-requirements.txt +++ b/lint-requirements.txt @@ -20,9 +20,9 @@ iniconfig==2.0.0 # via pytest isort==5.13.2 # via -r lint-requirements.in -mypy==0.910 +mypy==1.10.1 # via -r lint-requirements.in -mypy-extensions==0.4.4 +mypy-extensions==1.0.0 # via # black # mypy @@ -44,5 +44,5 @@ toml==0.10.2 # via mypy types-pyopenssl==24.0.0.20240228 # via -r lint-requirements.in -typing-extensions==4.10.0 +typing-extensions==4.12.2 # via mypy diff --git a/src/trustme/__init__.py b/src/trustme/__init__.py index 3996535..ff87ab7 100644 --- a/src/trustme/__init__.py +++ b/src/trustme/__init__.py @@ -62,7 +62,7 @@ def random_text() -> str: def _smells_like_pyopenssl(ctx: object) -> bool: - return getattr(ctx, "__module__", "").startswith("OpenSSL") # type: ignore[no-any-return] + return getattr(ctx, "__module__", "").startswith("OpenSSL") def _cert_builder_common( diff --git a/tests/test_trustme.py b/tests/test_trustme.py index eee0a39..1a1e31f 100644 --- a/tests/test_trustme.py +++ b/tests/test_trustme.py @@ -9,7 +9,7 @@ import OpenSSL.SSL import pytest -import service_identity.pyopenssl # type: ignore[import] +import service_identity.pyopenssl # type: ignore[import-not-found] from cryptography import x509 from cryptography.hazmat.primitives.serialization import ( Encoding, From a775770ac31b2c4cabdca68f9d32ca3c37db1fde Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Jul 2024 08:12:07 +0400 Subject: [PATCH 8/9] Bump the dependencies group across 1 directory with 14 updates (#657) --- docs-requirements.txt | 12 ++++++------ lint-requirements.txt | 20 +++++++++++--------- test-requirements.txt | 18 ++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index 8246ce2..279fb75 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -6,7 +6,7 @@ # alabaster==0.7.16 # via sphinx -babel==2.14.0 +babel==2.15.0 # via sphinx certifi==2024.7.4 # via requests @@ -28,17 +28,17 @@ jinja2==3.1.2 # via sphinx markupsafe==2.1.1 # via jinja2 -packaging==23.2 +packaging==24.1 # via sphinx -pycparser==2.21 +pycparser==2.22 # via cffi -pygments==2.17.2 +pygments==2.18.0 # via sphinx -requests==2.31.0 +requests==2.32.3 # via sphinx snowballstemmer==2.2.0 # via sphinx -sphinx==7.2.6 +sphinx==7.3.7 # via sphinxcontrib-trio sphinxcontrib-applehelp==1.0.2 # via sphinx diff --git a/lint-requirements.txt b/lint-requirements.txt index c7c3dfb..2829bfc 100644 --- a/lint-requirements.txt +++ b/lint-requirements.txt @@ -4,7 +4,7 @@ # # pip-compile lint-requirements.in # -black==24.2.0 +black==24.4.2 # via -r lint-requirements.in cffi==1.16.0 # via cryptography @@ -26,23 +26,25 @@ mypy-extensions==1.0.0 # via # black # mypy -packaging==23.2 +packaging==24.1 # via # black # pytest pathspec==0.12.1 # via black -platformdirs==4.2.0 +platformdirs==4.2.2 # via black -pluggy==1.4.0 +pluggy==1.5.0 # via pytest -pycparser==2.21 +pycparser==2.22 # via cffi -pytest==8.0.2 +pytest==8.2.2 # via -r lint-requirements.in -toml==0.10.2 - # via mypy -types-pyopenssl==24.0.0.20240228 +types-cffi==1.16.0.20240331 + # via types-pyopenssl +types-pyopenssl==24.1.0.20240425 # via -r lint-requirements.in +types-setuptools==70.2.0.20240704 + # via types-cffi typing-extensions==4.12.2 # via mypy diff --git a/test-requirements.txt b/test-requirements.txt index cca3e89..bd8fe38 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,10 +8,8 @@ attrs==23.2.0 # via service-identity cffi==1.16.0 # via cryptography -coverage[toml]==7.4.3 - # via - # -r test-requirements.in - # coverage +coverage[toml]==7.5.4 + # via -r test-requirements.in cryptography==41.0.7 # via # -r test-requirements.in @@ -21,21 +19,21 @@ idna==3.4 # via -r test-requirements.in iniconfig==2.0.0 # via pytest -packaging==23.2 +packaging==24.1 # via pytest -pluggy==1.4.0 +pluggy==1.5.0 # via pytest pyasn1==0.5.1 # via # pyasn1-modules # service-identity -pyasn1-modules==0.3.0 +pyasn1-modules==0.4.0 # via service-identity -pycparser==2.21 +pycparser==2.22 # via cffi -pyopenssl==24.0.0 +pyopenssl==24.1.0 # via -r test-requirements.in -pytest==8.0.2 +pytest==8.2.2 # via -r test-requirements.in service-identity==24.1.0 # via -r test-requirements.in From 4044d7fe7a33b8429cf9e6fb9ad4472f874b811e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Jul 2024 17:34:53 +0400 Subject: [PATCH 9/9] Bump black from 24.2.0 to 24.4.2 (#646) * Bump black from 24.2.0 to 24.4.2 Bumps [black](https://github.com/psf/black) from 24.2.0 to 24.4.2. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/24.2.0...24.4.2) --- updated-dependencies: - dependency-name: black dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Run black --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Quentin Pradet