Skip to content

Commit

Permalink
replace expires with not_after in CertificateAuthorityManager.init()
Browse files Browse the repository at this point in the history
  • Loading branch information
mathiasertl committed Oct 5, 2024
1 parent d902d27 commit 614efe6
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 48 deletions.
10 changes: 5 additions & 5 deletions ca/django_ca/management/commands/init_ca.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,13 +387,13 @@ def handle( # pylint: disable=too-many-locals # noqa: PLR0912,PLR0913,PLR0915
# The reasoning is simple: When issuing the child CA, the default is automatically after that of the
# parent if it wasn't issued on the same day.
if parent and timezone.now() + expires > parent.expires:
expires_datetime = parent.expires
not_after_datetime = parent.expires

# Make sure expires_datetime is tz-aware, even if USE_TZ=False.
if timezone.is_naive(expires_datetime):
expires_datetime = timezone.make_aware(expires_datetime)
if timezone.is_naive(not_after_datetime):
not_after_datetime = timezone.make_aware(not_after_datetime)
else:
expires_datetime = datetime.now(tz=tz.utc) + expires
not_after_datetime = datetime.now(tz=tz.utc) + expires

if parent and not parent.allows_intermediate_ca:
raise CommandError("Parent CA cannot create intermediate CA due to path length restrictions.")
Expand Down Expand Up @@ -549,7 +549,7 @@ def handle( # pylint: disable=too-many-locals # noqa: PLR0912,PLR0913,PLR0915
key_backend=key_backend,
key_backend_options=key_backend_options,
subject=parsed_subject,
expires=expires_datetime,
not_after=not_after_datetime,
algorithm=algorithm,
parent=parent,
use_parent_private_key_options=signer_key_backend_options,
Expand Down
44 changes: 29 additions & 15 deletions ca/django_ca/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,16 @@ def _handle_crl_distribution_point(
),
)

@deprecate_argument("expires", RemovedInDjangoCA230Warning, replacement="not_after")
def init( # noqa: PLR0912,PLR0913,PLR0915
self,
name: str,
# If BaseModel is used, you can no longer pass subclasses without a mypy warning (-> variance)
key_backend: KeyBackend[Any, Any, Any],
key_backend_options: BaseModel,
subject: x509.Name,
expires: datetime,
not_after: Optional[datetime] = None,
expires: Optional[datetime] = None,
algorithm: Optional[AllowedHashTypes] = None,
parent: Optional["CertificateAuthority"] = None,
use_parent_private_key_options: Optional[BaseModel] = None,
Expand All @@ -238,15 +240,20 @@ def init( # noqa: PLR0912,PLR0913,PLR0915
) -> "CertificateAuthority":
"""Create a new certificate authority.
.. versionchanged:: 1.29.0
.. deprecated:: 2.1.0
* The `expires` parameter is now mandatory, passing ``None`` will raise ``ValueError``.
The ``expires`` parameter is deprecated and will be removed in django-ca 2.3.0. use ``not_after``
instead.
.. deprecated:: 1.29.0
.. versionchanged:: 2.0.0
* Support for passing an ``int`` or ``timedelta`` for `expires` has been deprecated and will be
removed in django-ca 2.0.
.. versionchanged:: 1.29.0
* The `expires` parameter is now mandatory, passing ``None`` will raise ``ValueError``.
.. versionchanged:: 1.28.0
* The `key_backend` and `key_backend_options` parameters where added.
Expand Down Expand Up @@ -275,7 +282,7 @@ def init( # noqa: PLR0912,PLR0913,PLR0915
Parameters required for creating the private key using `key_backend`.
subject : :py:class:`cg:cryptography.x509.Name`
The desired subject for the certificate.
expires : datetime
not_after : datetime
When this certificate authority will expire.
algorithm : :py:class:`~cg:cryptography.hazmat.primitives.hashes.HashAlgorithm`, optional
Hash algorithm used when signing the certificate, defaults to
Expand Down Expand Up @@ -336,6 +343,13 @@ def init( # noqa: PLR0912,PLR0913,PLR0915
For various cases of wrong input data (e.g. extensions of invalid type).
"""
# pylint: disable=too-many-locals
if expires is not None and not_after is not None:
raise ValueError("`not_before` and `expires` cannot both be set.")
if not_after is None:
not_after = expires
if not_after is None: # pragma: only django-ca<2.3.0 # can only happen while we still have expires
raise TypeError("Missing required argument: 'not_after'")

if parent is not None and use_parent_private_key_options is None:
raise ValueError("use_parent_private_key_options is required when parent is passed.")
if openssh_ca and parent:
Expand All @@ -353,10 +367,10 @@ def init( # noqa: PLR0912,PLR0913,PLR0915
# NOTE: Already verified by the caller, so this is only for when the Python API is used directly.
algorithm = validate_public_key_parameters(key_type, algorithm)

if not isinstance(expires, datetime):
raise TypeError(f"{expires}: expires must be a datetime.")
if expires.utcoffset() is None:
raise ValueError("expires must not be a naive datetime.")
if not isinstance(not_after, datetime):
raise TypeError(f"{not_after}: not_after must be a datetime.")
if not_after.utcoffset() is None:
raise ValueError("not_after must not be a naive datetime.")

# Append OpenSSH extensions if an OpenSSH CA was requested
if openssh_ca:
Expand Down Expand Up @@ -477,7 +491,7 @@ def init( # noqa: PLR0912,PLR0913,PLR0915
name=name,
key_type=key_type,
algorithm=algorithm,
expires=expires,
not_after=not_after,
parent=parent,
subject=subject,
path_length=path_length,
Expand Down Expand Up @@ -543,11 +557,11 @@ def init( # noqa: PLR0912,PLR0913,PLR0915
signer_ca,
use_private_key_options,
public_key,
serial,
algorithm,
issuer,
subject,
expires,
serial=serial,
algorithm=algorithm,
issuer=issuer,
subject=subject,
expires=not_after,
extensions=extensions,
)

Expand Down
2 changes: 1 addition & 1 deletion ca/django_ca/tests/base/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ def usable_hsm_ca( # pragma: hsm
key_backend_options=key_backend_options,
key_type=key_type,
subject=subject,
expires=datetime.now(tz=timezone.utc) + timedelta(days=720),
not_after=datetime.now(tz=timezone.utc) + timedelta(days=720),
)
assert isinstance(ca.key_backend, HSMBackend)
yield ca
Expand Down
73 changes: 57 additions & 16 deletions ca/django_ca/tests/test_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def test_init_intermediate(
key_backend,
key_backend_options,
subject,
expires=datetime.now(tz=tz.utc) + timedelta(days=10),
not_after=datetime.now(tz=tz.utc) + timedelta(days=10),
parent=usable_root,
use_parent_private_key_options=parent_key_backend_options,
)
Expand All @@ -165,7 +165,7 @@ def test_init_grandchild(
key_backend,
key_backend_options,
subject,
expires=datetime.now(tz=tz.utc) + timedelta(days=10),
not_after=datetime.now(tz=tz.utc) + timedelta(days=10),
parent=usable_child,
use_parent_private_key_options=parent_key_backend_options,
)
Expand Down Expand Up @@ -212,7 +212,7 @@ def test_openssh_ca_for_intermediate(
key_backend,
ca_key_backend_options,
subject=subject,
expires=datetime.now(tz=tz.utc) + timedelta(days=10),
not_after=datetime.now(tz=tz.utc) + timedelta(days=10),
key_type="Ed25519",
parent=root,
use_parent_private_key_options=parent_key_backend_options,
Expand All @@ -231,7 +231,7 @@ def test_init_with_no_default_hostname(
key_backend,
key_backend_options,
subject,
expires=datetime.now(tz=tz.utc) + timedelta(days=10),
not_after=datetime.now(tz=tz.utc) + timedelta(days=10),
parent=usable_child,
use_parent_private_key_options=parent_key_backend_options,
default_hostname=False,
Expand Down Expand Up @@ -288,7 +288,7 @@ def test_init_with_partial_authority_information_access(
key_backend,
key_backend_options,
subject,
expires=datetime.now(tz=tz.utc) + timedelta(days=10),
not_after=datetime.now(tz=tz.utc) + timedelta(days=10),
parent=usable_root,
use_parent_private_key_options=parent_key_backend_options,
extensions=passed_extensions,
Expand All @@ -307,7 +307,7 @@ def test_init_with_partial_authority_information_access(
key_backend,
key_backend_options,
subject,
expires=datetime.now(tz=tz.utc) + timedelta(days=10),
not_after=datetime.now(tz=tz.utc) + timedelta(days=10),
parent=usable_root,
use_parent_private_key_options=parent_key_backend_options,
extensions=passed_extensions,
Expand Down Expand Up @@ -336,7 +336,7 @@ def test_init_with_formatting(
key_backend,
key_backend_options,
subject,
expires=datetime.now(tz=tz.utc) + timedelta(days=10),
not_after=datetime.now(tz=tz.utc) + timedelta(days=10),
parent=usable_root,
use_parent_private_key_options=parent_key_backend_options,
extensions=passed_extensions,
Expand Down Expand Up @@ -453,32 +453,73 @@ def test_init_with_api_parameters(ca_name: str, subject: x509.Name, key_backend:
assert_certificate(ca, subject)


def test_init_with_expires_is_none(ca_name: str, subject: x509.Name, key_backend: StoragesBackend) -> None:
def test_init_with_expires_is_wrong_type(
ca_name: str, subject: x509.Name, key_backend: StoragesBackend
) -> None:
"""Test init with an expired as None."""
with pytest.raises(TypeError, match=r"^None: expires must be a datetime\."):
with pytest.raises(TypeError, match=r"^3: not_after must be a datetime\."):
CertificateAuthority.objects.init(
ca_name,
key_backend,
key_backend_options,
subject,
expires=None, # type: ignore[arg-type] # what we're testing
not_after=3, # type: ignore[arg-type] # what we're testing
)


def test_init_with_naive_expires(ca_name: str, subject: x509.Name, key_backend: StoragesBackend) -> None:
"""Test init with a naive expired."""
expires = datetime(2024, 5, 31)
with pytest.raises(ValueError, match=r"^expires must not be a naive datetime\."):
CertificateAuthority.objects.init(ca_name, key_backend, key_backend_options, subject, expires=expires)
not_after = datetime(2024, 5, 31)
with pytest.raises(ValueError, match=r"^not_after must not be a naive datetime\."):
CertificateAuthority.objects.init(
ca_name, key_backend, key_backend_options, subject, not_after=not_after
)


def test_init_with_unknown_profile(ca_name: str, subject: x509.Name, key_backend: StoragesBackend) -> None:
"""Create a CA with a profile that doesn't exist."""
expires = datetime.now(tz=tz.utc) + timedelta(days=10)
not_after = datetime.now(tz=tz.utc) + timedelta(days=10)
with pytest.raises(ValueError, match=r"^foobar: Profile is not defined\.$"):
CertificateAuthority.objects.init(
ca_name, key_backend, key_backend_options, subject, expires, acme_profile="foobar"
ca_name, key_backend, key_backend_options, subject, not_after, acme_profile="foobar"
)


def test_init_with_not_after_and_expires(
ca_name: str, subject: x509.Name, key_backend: StoragesBackend
) -> None:
"""Create a CA with both not_after and expires, which is an error."""
not_after = datetime.now(tz=tz.utc) + timedelta(days=10)
warning = (
r"^Argument `expires` is deprecated and will be removed in django-ca 2.3, use `not_after` instead\.$"
)
error = r"^`not_before` and `expires` cannot both be set\.$"
with pytest.warns(RemovedInDjangoCA230Warning, match=warning), pytest.raises(ValueError, match=error):
CertificateAuthority.objects.init(
ca_name, key_backend, key_backend_options, subject, not_after=not_after, expires=not_after
)


def test_init_with_not_after_is_none(ca_name: str, subject: x509.Name, key_backend: StoragesBackend) -> None:
"""Pass ``None`` for not_after, which is checked until 2.3.0."""
with pytest.raises(TypeError, match=r"^Missing required argument: 'not_after'$"):
CertificateAuthority.objects.init(ca_name, key_backend, key_backend_options, subject, not_after=None)


@pytest.mark.django_db
def test_init_with_deprecated_expires(ca_name: str, subject: x509.Name, key_backend: StoragesBackend) -> None:
"""Create a CA with deprecated expires parameter."""
not_after = datetime.now(tz=tz.utc) + timedelta(days=10)
not_after = not_after.replace(microsecond=0, second=0)
warning = (
r"^Argument `expires` is deprecated and will be removed in django-ca 2.3, use `not_after` instead\.$"
)
with pytest.warns(RemovedInDjangoCA230Warning, match=warning):
ca = CertificateAuthority.objects.init(
ca_name, key_backend, key_backend_options, subject, expires=not_after
)
assert ca.expires == not_after
assert ca.pub.loaded.not_valid_after_utc == not_after


@pytest.mark.django_db
Expand All @@ -490,7 +531,7 @@ def test_init_with_unknown_extension_type(subject: x509.Name, key_backend: Stora
key_backend,
key_backend_options,
subject,
expires=datetime.now(tz=tz.utc) + timedelta(days=10),
not_after=datetime.now(tz=tz.utc) + timedelta(days=10),
extensions=[True], # type: ignore[list-item] # what we are testing
)
assert CertificateAuthority.objects.count() == 0
Expand Down
1 change: 1 addition & 0 deletions ca/django_ca/tests/test_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,7 @@ def test_create_cert_with_deprecated_expires(usable_root: CertificateAuthority,
with pytest.warns(RemovedInDjangoCA230Warning, match=warning):
cert = create_cert(prof, usable_root, csr, subject=subject, expires=not_after)
assert cert.expires == not_after
assert cert.pub.loaded.not_valid_after_utc == not_after


def test_create_cert_with_not_after_and_deprecated_expires(
Expand Down
2 changes: 1 addition & 1 deletion ca/django_ca/tests/test_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def assert_scope(
def init_ca(name: str, **kwargs: Any) -> CertificateAuthority:
"""Create a CA."""
subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)])
kwargs.setdefault("expires", datetime.now(tz=tz.utc) + timedelta(days=365 * 2))
kwargs.setdefault("not_after", datetime.now(tz=tz.utc) + timedelta(days=365 * 2))
key_backend = key_backends["default"]
key_backend_options = StoragesCreatePrivateKeyOptions(
key_type="RSA", password=None, path="ca", key_size=1024
Expand Down
3 changes: 2 additions & 1 deletion docs/source/changelog/TBR_2.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ Dependencies
Python API
**********

* :py:func:`CertificateManager.create_cert() <django_ca.managers.CertificateManager.create_cert>` and
* :py:func:`CertificateAuthorityManager.init() <django_ca.managers.CertificateAuthorityManager.init>`,
:py:func:`CertificateManager.create_cert() <django_ca.managers.CertificateManager.create_cert>` and
:func:`Profile.create_cert() <django_ca.profiles.Profile.create_cert>` now takes a ``not_after`` parameter,
replacing ``expires``. The latter is deprecated and will be removed in django-ca 2.3.0.

Expand Down
3 changes: 2 additions & 1 deletion docs/source/deprecation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Deprecation timeline
Python API
==========

* The ``expires`` parameter to :py:func:`CertificateManager.create_cert()
* The ``expires`` parameter to :py:func:`CertificateAuthorityManager.init()
<django_ca.managers.CertificateAuthorityManager.init>`, :py:func:`CertificateManager.create_cert()
<django_ca.managers.CertificateManager.create_cert>` and :func:`Profile.create_cert()
<django_ca.profiles.Profile.create_cert>` will be removed. Use ``not_after`` instead (deprecated since
2.1.0).
Expand Down
4 changes: 2 additions & 2 deletions docs/source/python/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ creates a minimal CA using the file system storage backend::
>>> key_backend_options = StoragesCreatePrivateKeyOptions(
... key_type="RSA", key_size=1024, password=None, path="ca"
... )
>>> expires = datetime.now(tz=timezone.utc) + timedelta(days=365 * 10)
>>> not_after = datetime.now(tz=timezone.utc) + timedelta(days=365 * 10)
>>> CertificateAuthority.objects.init(
... name="ca-two",
... key_backend=key_backends["default"],
... key_backend_options=key_backend_options,
... subject=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "ca.example.com")]),
... expires=expires,
... not_after=not_after,
... )
<CertificateAuthority: ca-two>

Expand Down
Loading

0 comments on commit 614efe6

Please sign in to comment.