Skip to content
Draft
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
2 changes: 2 additions & 0 deletions newsfragments/727.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
``CA.from_pem`` now preserves the imported certificate's CA path-length
constraint when creating child CAs, including an unlimited path length.
13 changes: 11 additions & 2 deletions src/trustme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ class CA:
def __init__(
self,
parent_cert: Optional[CA] = None,
path_length: int = 9,
path_length: Optional[int] = 9,
organization_name: Optional[str] = None,
organization_unit_name: Optional[str] = None,
key_type: KeyType = KeyType.ECDSA,
Expand Down Expand Up @@ -317,7 +317,7 @@ def create_child_ca(self, key_type: KeyType = KeyType.ECDSA) -> "CA":
if self._path_length == 0:
raise ValueError("Can't create child CA: path length is 0")

path_length = self._path_length - 1
path_length = self._path_length - 1 if self._path_length is not None else None
return CA(parent_cert=self, path_length=path_length, key_type=key_type)

def issue_cert(
Expand Down Expand Up @@ -499,6 +499,15 @@ def from_pem(cls, cert_bytes: bytes, private_key_bytes: bytes) -> "CA":
ca.parent_cert = None
ca._certificate = x509.load_pem_x509_certificate(cert_bytes)
ca._private_key = load_pem_private_key(private_key_bytes, password=None) # type: ignore[assignment]
try:
constraints = ca._certificate.extensions.get_extension_for_class(
x509.BasicConstraints
)
except x509.ExtensionNotFound:
# Preserve the default for older certificates without constraints.
pass
else:
ca._path_length = constraints.value.path_length

return ca

Expand Down
41 changes: 41 additions & 0 deletions tests/test_trustme.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import pytest
import service_identity.pyopenssl # type: ignore[import-not-found]
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.serialization import (
Encoding,
PublicFormat,
Expand Down Expand Up @@ -284,6 +285,46 @@ def test_ca_from_pem(tmp_path: Path) -> None:
assert ca1.private_key_pem.bytes() == ca2.private_key_pem.bytes()


@pytest.mark.parametrize("path_length", [0, 1, 2, 9, None])
def test_ca_from_pem_path_length(path_length: Optional[int]) -> None:
ca = CA(path_length=path_length)
restored = CA.from_pem(ca.cert_pem.bytes(), ca.private_key_pem.bytes())

# An unconstrained CA must also remain unconstrained after import,
# including beyond the default depth of nine intermediates.
for depth in range(path_length if path_length is not None else 11):
restored = restored.create_child_ca()
cert = x509.load_pem_x509_certificate(restored.cert_pem.bytes())
expected = path_length - depth - 1 if path_length is not None else None
assert _path_length(cert) == expected

if path_length is not None:
with pytest.raises(ValueError, match="path length is 0"):
restored.create_child_ca()


def test_ca_from_pem_without_basic_constraints() -> None:
ca = CA()
original = ca._certificate
cert = (
x509.CertificateBuilder()
.subject_name(original.subject)
.issuer_name(original.issuer)
.public_key(ca._private_key.public_key())
.serial_number(original.serial_number)
.not_valid_before(original.not_valid_before_utc)
.not_valid_after(original.not_valid_after_utc)
.add_extension(
x509.SubjectKeyIdentifier.from_public_key(ca._private_key.public_key()),
critical=False,
)
.sign(ca._private_key, hashes.SHA256())
)
restored = CA.from_pem(cert.public_bytes(Encoding.PEM), ca.private_key_pem.bytes())
child = restored.create_child_ca()
assert _path_length(x509.load_pem_x509_certificate(child.cert_pem.bytes())) == 8


def check_connection_end_to_end(
wrap_client: Callable[[CA, socket.socket, str], SslSocket],
wrap_server: Callable[[LeafCert, socket.socket], SslSocket],
Expand Down
Loading