diff --git a/docs-requirements.txt b/docs-requirements.txt index 9a75050..422b5be 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -6,9 +6,9 @@ # alabaster==0.7.16 # via sphinx -babel==2.14.0 +babel==2.15.0 # via sphinx -certifi==2023.11.17 +certifi==2024.7.4 # via requests cffi==1.16.0 # via cryptography @@ -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 @@ -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.2 # via requests diff --git a/lint-requirements.in b/lint-requirements.in index 0d7337d..9a38e30 100644 --- a/lint-requirements.in +++ b/lint-requirements.in @@ -1,5 +1,7 @@ -mypy==0.910 +mypy 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 7f90104..a56b231 100644 --- a/lint-requirements.txt +++ b/lint-requirements.txt @@ -1,34 +1,50 @@ # -# 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.4.2 + # via -r lint-requirements.in cffi==1.16.0 # via cryptography +click==8.1.7 + # via black cryptography==42.0.4 # via # -r lint-requirements.in # types-pyopenssl -idna==3.4 +idna==3.6 # via -r lint-requirements.in iniconfig==2.0.0 # via pytest -mypy==0.910 +isort==5.13.2 # via -r lint-requirements.in -mypy-extensions==0.4.4 - # via mypy -packaging==23.2 - # via pytest -pluggy==1.4.0 +mypy==1.10.1 + # via -r lint-requirements.in +mypy-extensions==1.0.0 + # via + # black + # mypy +packaging==24.1 + # via + # black + # pytest +pathspec==0.12.1 + # via black +platformdirs==4.2.2 + # via black +pluggy==1.5.0 # via pytest -pycparser==2.21 +pycparser==2.22 # via cffi -pytest==8.0.0 +pytest==8.2.2 # via -r lint-requirements.in -toml==0.10.2 - # via mypy -types-pyopenssl==24.0.0.20240130 +types-cffi==1.16.0.20240331 + # via types-pyopenssl +types-pyopenssl==24.1.0.20240425 # via -r lint-requirements.in -typing-extensions==4.9.0 +types-setuptools==70.2.0.20240704 + # via types-cffi +typing-extensions==4.12.2 # via mypy 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/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..ff87ab7 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) @@ -58,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( @@ -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__( @@ -246,34 +250,43 @@ 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 = ( - _cert_builder_common(name, issuer, self._private_key.public_key()) - .add_extension( - x509.BasicConstraints(ca=True, path_length=path_length), - critical=True, - ) - .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 + ski_ext = parent_certificate.extensions.get_extension_for_class( + x509.SubjectKeyIdentifier ) - .sign( - private_key=sign_key, - algorithm=hashes.SHA256(), + 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, + ) + 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(), ) @property @@ -288,11 +301,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 @@ -369,15 +380,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( @@ -412,16 +424,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, @@ -436,14 +451,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 @@ -457,18 +472,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": @@ -508,12 +522,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. @@ -528,18 +545,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/test-requirements.txt b/test-requirements.txt index 9135628..3809e21 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ attrs==23.2.0 # via service-identity cffi==1.16.0 # via cryptography -coverage[toml]==7.4.1 +coverage[toml]==7.5.4 # via -r test-requirements.in cryptography==42.0.4 # via @@ -19,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.0 +pytest==8.2.2 # via -r test-requirements.in service-identity==24.1.0 # via -r test-requirements.in 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 3980442..93275bc 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 +import OpenSSL.SSL +import pytest +import service_identity.pyopenssl # type: ignore[import-not-found] from cryptography import x509 from cryptography.hazmat.primitives.serialization import ( - Encoding, PublicFormat, load_pem_private_key) - -import OpenSSL.SSL -import service_identity.pyopenssl # type: ignore[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: @@ -200,6 +198,12 @@ 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 @@ -272,6 +276,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()) @@ -354,11 +359,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 @@ -366,11 +372,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 @@ -379,12 +386,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() @@ -392,7 +402,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) @@ -414,38 +426,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"), } @@ -457,9 +461,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 @@ -481,27 +483,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"