diff --git a/.github/workflows/faketime.yml b/.github/workflows/faketime.yml new file mode 100644 index 000000000..8e88eb146 --- /dev/null +++ b/.github/workflows/faketime.yml @@ -0,0 +1,38 @@ +name: Tests with faked time +on: + push: + pull_request: + types: [opened, reopened] + pull_request_target: + +jobs: + run: + runs-on: ubuntu-latest + + name: libfaketime test + steps: + - name: Install APT dependencies + run: sudo apt-get install -y firefox faketime + + - name: Acquire sources + uses: actions/checkout@v4.1.1 + + - name: Setup Python + uses: actions/setup-python@v5.0.0 + with: + python-version: 3.12 + architecture: x64 + + - name: Apply caching of dependencies + uses: actions/cache@v4.0.0 + with: + path: ~/.cache/pip + key: pip-${{ hashFiles('**/requirements-*.txt') }} + + - name: Install dependencies + run: | + pip install -U pip setuptools wheel + pip install -r requirements.txt -r requirements/requirements-test.txt + + - name: Run tests + run: faketime -f +100y pytest -v --cov-report term-missing --durations=20 diff --git a/ca/django_ca/admin.py b/ca/django_ca/admin.py index c56fbeb95..732edee70 100644 --- a/ca/django_ca/admin.py +++ b/ca/django_ca/admin.py @@ -1120,6 +1120,7 @@ def lookups( # type: ignore # we are more specific here def queryset(self, request: HttpRequest, queryset: QuerySetTypeVar) -> QuerySetTypeVar: now = timezone.now() + print("###", now) if self.value() == "0": return queryset.filter(expires__gt=now) diff --git a/ca/django_ca/tests/admin/assertions.py b/ca/django_ca/tests/admin/assertions.py index bbb5728c0..c613c3a2b 100644 --- a/ca/django_ca/tests/admin/assertions.py +++ b/ca/django_ca/tests/admin/assertions.py @@ -44,6 +44,9 @@ def assert_changelist_response(response: "HttpResponse", *objects: models.Model) def sorter(obj: models.Model) -> Any: return obj.pk + print("### found", response.context["cl"].result_list) + for found in response.context["cl"].result_list: + print(found.expires) assert sorted(response.context["cl"].result_list, key=sorter) == sorted(objects, key=sorter) templates = [t.name for t in response.templates] assert "admin/base.html" in templates diff --git a/ca/django_ca/tests/admin/test_add_cert.py b/ca/django_ca/tests/admin/test_add_cert.py index 44913b3b4..e310e2759 100644 --- a/ca/django_ca/tests/admin/test_add_cert.py +++ b/ca/django_ca/tests/admin/test_add_cert.py @@ -1348,7 +1348,7 @@ def test_only_ca_prefill(self) -> None: cn = "test-only-ca.example.com" assert self.ca.sign_authority_information_access is not None assert self.ca.sign_crl_distribution_points is not None - self.ca.sign_certificate_policies = self.certificate_policies( + self.ca.sign_certificate_policies = certificate_policies( x509.PolicyInformation( policy_identifier=x509.ObjectIdentifier("1.2.3"), policy_qualifiers=[ diff --git a/ca/django_ca/tests/admin/test_admin_ca.py b/ca/django_ca/tests/admin/test_admin_ca.py index 5c241959a..438fc04f4 100644 --- a/ca/django_ca/tests/admin/test_admin_ca.py +++ b/ca/django_ca/tests/admin/test_admin_ca.py @@ -23,6 +23,7 @@ from django_ca.models import CertificateAuthority from django_ca.tests.admin.assertions import assert_change_response from django_ca.tests.base.mixins import AdminTestCaseMixin, StandardAdminViewTestCaseMixin +from django_ca.tests.base.utils import certificate_policies class CertificateAuthorityAdminViewTestCase(StandardAdminViewTestCaseMixin[CertificateAuthority], TestCase): @@ -55,7 +56,7 @@ def test_complex_sign_certificate_policies(self) -> None: # used for that extension. self.assertNotIn(ExtensionOID.CERTIFICATE_POLICIES, ca.extensions) - ca.sign_certificate_policies = self.certificate_policies( + ca.sign_certificate_policies = certificate_policies( x509.PolicyInformation( policy_identifier=CertificatePoliciesOID.ANY_POLICY, policy_qualifiers=[ diff --git a/ca/django_ca/tests/base/conftest_helpers.py b/ca/django_ca/tests/base/conftest_helpers.py index 5a843706e..83f361841 100644 --- a/ca/django_ca/tests/base/conftest_helpers.py +++ b/ca/django_ca/tests/base/conftest_helpers.py @@ -162,7 +162,10 @@ def fixture( db: Any, # pylint: disable=unused-argument # usefixtures does not work for fixtures ) -> Iterator[CertificateAuthority]: data = CERT_DATA[name] - pub = request.getfixturevalue(f"{name}_pub") + ca_fixture_name = f"{name}_pub" + if data["cat"] == "sphinx-contrib": + ca_fixture_name = f"contrib_{ca_fixture_name}" + pub = request.getfixturevalue(ca_fixture_name) # Load any parent parent = None @@ -170,8 +173,8 @@ def fixture( parent = request.getfixturevalue(parent_name) kwargs = { - "sign_crl_distribution_points": data["sign_crl_distribution_points"], - "sign_authority_information_access": data["sign_authority_information_access"], + "sign_crl_distribution_points": data.get("sign_crl_distribution_points"), + "sign_authority_information_access": data.get("sign_authority_information_access"), } ca = load_ca(name, pub, parent, **kwargs) @@ -204,8 +207,16 @@ def generate_cert_fixture(name: str) -> typing.Callable[["SubRequest"], Iterator def fixture(request: "SubRequest") -> Iterator[Certificate]: sanitized_name = name.replace("-", "_") data = CERT_DATA[name] - ca = request.getfixturevalue(data["ca"]) - pub = request.getfixturevalue(f"{sanitized_name}_pub") + + ca_fixture_name = data["ca"] + if data["cat"] == "sphinx-contrib": + ca_fixture_name = f"contrib_{ca_fixture_name}" + ca = request.getfixturevalue(ca_fixture_name) + + pub_fixture_name = f"{sanitized_name}_pub" + if data["cat"] in ("contrib", "sphinx-contrib"): + pub_fixture_name = f"contrib_{pub_fixture_name}" + pub = request.getfixturevalue(pub_fixture_name) cert = load_cert(ca, None, pub, data.get("profile", "")) yield cert # NOTE: Yield must be outside the freeze-time block, or durations are wrong @@ -219,8 +230,11 @@ def load_pub(name: str) -> x509.Certificate: if conf["cat"] == "sphinx-contrib": with open(conf["pub_path"], "rb") as stream: return x509.load_pem_x509_certificate(stream.read()) + if conf["cat"] == "contrib": + with open(FIXTURES_DIR / "contrib" / f"{name}.pub", "rb") as stream: + return x509.load_der_x509_certificate(stream.read()) else: - with open(os.path.join(FIXTURES_DIR, f"{name}.pub"), "rb") as stream: + with open(FIXTURES_DIR / f"{name}.pub", "rb") as stream: return x509.load_der_x509_certificate(stream.read()) @@ -290,6 +304,9 @@ def load_cert( usable_ca_names = [ name for name, conf in CERT_DATA.items() if conf["type"] == "ca" and conf.get("key_filename") ] +contrib_ca_names = [ + name for name, conf in CERT_DATA.items() if conf["type"] == "ca" and conf["cat"] == "sphinx-contrib" +] unusable_ca_names = [ name for name, conf in CERT_DATA.items() if conf["type"] == "ca" and name not in usable_ca_names ] @@ -302,6 +319,11 @@ def load_cert( usable_cert_names = [ name for name, conf in CERT_DATA.items() if conf["type"] == "cert" and conf["cat"] == "generated" ] +contrib_cert_names = [ + name + for name, conf in CERT_DATA.items() + if conf["type"] == "cert" and conf["cat"] in ("contrib", "sphinx-contrib") +] unusable_cert_names = [ name for name, conf in CERT_DATA.items() if conf["type"] == "cert" and name not in usable_ca_names ] diff --git a/ca/django_ca/tests/base/constants.py b/ca/django_ca/tests/base/constants.py index 6619e071a..0eb0f62d4 100644 --- a/ca/django_ca/tests/base/constants.py +++ b/ca/django_ca/tests/base/constants.py @@ -117,7 +117,7 @@ def _load_latest_version(versions: list[str]) -> tuple[int, int]: "cn": "", "key_filename": False, "csr_filename": False, - "pub_filename": os.path.join("contrib", "multiple_ous_and_no_ext.pub"), + "pub_filename": os.path.join("contrib", "multiple_ous.pub"), "key_type": "RSA", "cat": "contrib", "type": "cert", diff --git a/ca/django_ca/tests/base/fixtures.py b/ca/django_ca/tests/base/fixtures.py index 487f020c1..b7eb5f9b1 100644 --- a/ca/django_ca/tests/base/fixtures.py +++ b/ca/django_ca/tests/base/fixtures.py @@ -23,6 +23,7 @@ from cryptography import x509 from cryptography.x509.oid import CertificatePoliciesOID, ExtensionOID, NameOID +from django.core.cache import cache from django.core.files.storage import storages import pytest @@ -34,13 +35,15 @@ from django_ca.key_backends.storages import StoragesBackend from django_ca.models import Certificate, CertificateAuthority from django_ca.tests.base.conftest_helpers import ( + all_ca_names, all_cert_names, - ca_cert_names, precertificate_signed_certificate_timestamps_cert_names, signed_certificate_timestamp_cert_names, signed_certificate_timestamps_cert_names, usable_ca_names, + usable_cert_names, ) +from django_ca.tests.base.constants import CERT_DATA @pytest.fixture(params=all_cert_names) @@ -206,8 +209,7 @@ def key_backend(request: "SubRequest") -> Iterator[StoragesBackend]: def precertificate_signed_certificate_timestamps_pub(request: "SubRequest") -> Iterator[x509.Certificate]: """Parametrized fixture for certificates that have a PrecertSignedCertificateTimestamps extension.""" name = request.param.replace("-", "_") - - yield request.getfixturevalue(f"{name}_pub") + yield request.getfixturevalue(f"contrib_{name}_pub") @pytest.fixture() @@ -231,8 +233,7 @@ def secondary_backend(request: "SubRequest") -> Iterator[StoragesBackend]: def signed_certificate_timestamp_pub(request: "SubRequest") -> Iterator[x509.Certificate]: """Parametrized fixture for certificates that have any SCT extension.""" name = request.param.replace("-", "_") - - yield request.getfixturevalue(f"{name}_pub") + yield request.getfixturevalue(f"contrib_{name}_pub") @pytest.fixture(params=signed_certificate_timestamps_cert_names) @@ -293,6 +294,15 @@ def tmpcadir(tmp_path: Path, settings: SettingsWrapper) -> Iterator[Path]: settings.STORAGES = orig_storages +@pytest.fixture(params=all_ca_names) +def ca(request: "SubRequest") -> Iterator[CertificateAuthority]: + """Parametrized fixture for all certificate authorities known to the test suite.""" + fixture_name = request.param + if CERT_DATA[fixture_name]["cat"] in ("contrib", "sphinx-contrib"): + fixture_name = f"contrib_{fixture_name}" + yield request.getfixturevalue(fixture_name) + + @pytest.fixture(params=usable_ca_names) def usable_ca_name(request: "SubRequest") -> Iterator[CertificateAuthority]: """Parametrized fixture for the name of every usable CA.""" @@ -314,9 +324,15 @@ def usable_cas(request: "SubRequest") -> Iterator[list[CertificateAuthority]]: yield cas -@pytest.fixture(params=ca_cert_names) +@pytest.fixture(params=usable_cert_names) def usable_cert(request: "SubRequest") -> Iterator[Certificate]: - """Parametrized fixture for every ``{ca}-cert`` certificate.""" - cert = request.getfixturevalue(request.param.replace("-", "_")) + """Parametrized fixture for every ``{ca}-cert`` certificate. + + The name of the certificate can be retrieved from the non-standard `test_name` property of the + certificate. + """ + name = request.param + cert = request.getfixturevalue(name.replace("-", "_")) + cert.test_name = name request.getfixturevalue(f"usable_{cert.ca.name}") yield cert diff --git a/ca/django_ca/tests/base/mixins.py b/ca/django_ca/tests/base/mixins.py index 7ce8c80ee..10a374e91 100644 --- a/ca/django_ca/tests/base/mixins.py +++ b/ca/django_ca/tests/base/mixins.py @@ -16,7 +16,6 @@ import copy import json import re -import textwrap import typing from collections.abc import Iterable, Iterator from contextlib import contextmanager @@ -37,7 +36,6 @@ from django.contrib.messages import get_messages from django.core.cache import cache from django.core.exceptions import ValidationError -from django.core.files.storage import storages from django.test.testcases import SimpleTestCase from django.urls import reverse @@ -46,14 +44,12 @@ from django_ca import ca_settings from django_ca.deprecation import crl_last_update, crl_next_update, revoked_certificate_revocation_date -from django_ca.extensions import extension_as_text from django_ca.models import Certificate, CertificateAuthority, DjangoCAModel, X509CertMixin from django_ca.signals import post_revoke_cert, post_sign_cert, pre_sign_cert from django_ca.tests.admin.assertions import assert_change_response, assert_changelist_response from django_ca.tests.base.constants import CERT_DATA, TIMESTAMPS from django_ca.tests.base.mocks import mock_signal from django_ca.tests.base.typehints import DjangoCAModelTypeVar -from django_ca.tests.base.utils import certificate_policies if typing.TYPE_CHECKING: # Use SimpleTestCase as base class when type checking. This way mypy will know about attributes/methods @@ -66,7 +62,7 @@ TestCaseProtocol = object -class TestCaseMixin(TestCaseProtocol): # pylint: disable=too-many-public-methods +class TestCaseMixin(TestCaseProtocol): """Mixin providing augmented functionality to all test cases.""" load_cas: Union[str, tuple[str, ...]] = tuple() @@ -345,27 +341,6 @@ def assertValidationError( # pylint: disable=invalid-name; unittest standard yield self.assertEqual(cmex.exception.message_dict, errors) - @property - def ca_certs(self) -> Iterator[tuple[str, Certificate]]: - """Yield loaded certificates for each certificate authority.""" - for name, cert in self.certs.items(): - if name in [ - "root-cert", - "child-cert", - "ec-cert", - "dsa-cert", - "pwd-cert", - "ed448-cert", - "ed25519-cert", - ]: - yield name, cert - - def certificate_policies( - self, *policies: x509.PolicyInformation, critical: bool = False - ) -> x509.Extension[x509.CertificatePolicies]: - """Shortcut for getting a Certificate Policy extension.""" - return certificate_policies(*policies, critical=critical) - def crl_distribution_points( self, full_name: Optional[Iterable[x509.GeneralName]] = None, @@ -446,39 +421,6 @@ def freeze_time( with freeze_time(timestamp) as frozen: yield frozen - def get_cert_context(self, name: str) -> dict[str, Any]: - """Get a dictionary suitable for testing output based on the dictionary in basic.certs.""" - ctx: dict[str, Any] = {} - - for key, value in sorted(CERT_DATA[name].items()): - # Handle cryptography extensions - if key == "extensions": - ctx["extensions"] = {ext["type"]: ext for ext in CERT_DATA[name].get("extensions", [])} - elif key == "precert_poison": - ctx["precert_poison"] = "* Precert Poison (critical):\n Yes" - elif isinstance(value, x509.Extension): - if value.critical: - ctx[f"{key}_critical"] = " (critical)" - else: - ctx[f"{key}_critical"] = "" - - ctx[f"{key}_text"] = textwrap.indent(extension_as_text(value.value), " ") - elif key == "path_length": - ctx[key] = value - ctx[f"{key}_text"] = "unlimited" if value is None else value - else: - ctx[key] = value - - if parent := CERT_DATA[name].get("parent"): - ctx["parent_name"] = CERT_DATA[parent]["name"] - ctx["parent_serial"] = CERT_DATA[parent]["serial"] - ctx["parent_serial_colons"] = CERT_DATA[parent]["serial_colons"] - - if CERT_DATA[name]["key_filename"] is not False: - storage = storages["django-ca"] - ctx["key_path"] = storage.path(CERT_DATA[name]["key_filename"]) - return ctx - @classmethod def load_ca( cls, @@ -586,13 +528,6 @@ def usable_cas(self) -> Iterator[tuple[str, CertificateAuthority]]: if CERT_DATA[name]["key_filename"]: yield name, ca - @property - def usable_certs(self) -> Iterator[tuple[str, Certificate]]: - """Yield loaded generated certificates.""" - for name, cert in self.certs.items(): - if CERT_DATA[name]["cat"] == "generated": - yield name, cert - class AdminTestCaseMixin(TestCaseMixin, typing.Generic[DjangoCAModelTypeVar]): """Common mixin for testing admin classes for models.""" diff --git a/ca/django_ca/tests/base/utils.py b/ca/django_ca/tests/base/utils.py index 2fb7780bd..8a2dd8ba0 100644 --- a/ca/django_ca/tests/base/utils.py +++ b/ca/django_ca/tests/base/utils.py @@ -21,6 +21,7 @@ import re import shutil import tempfile +import textwrap import typing from collections.abc import Iterable, Iterator from contextlib import contextmanager @@ -33,10 +34,12 @@ from cryptography.x509.oid import AuthorityInformationAccessOID, ExtensionOID from django.conf import settings +from django.core.files.storage import storages from django.core.management import ManagementUtility, call_command from django.test import override_settings from django.utils.crypto import get_random_string +from django_ca.extensions import extension_as_text from django_ca.models import CertificateAuthority, X509CertMixin from django_ca.profiles import profiles from django_ca.tests.base.constants import CERT_DATA, FIXTURES_DIR @@ -257,6 +260,40 @@ def freshest_crl( ) +def get_cert_context(name: str) -> dict[str, Any]: + """Get a dictionary suitable for testing output based on the dictionary in basic.certs.""" + ctx: dict[str, Any] = {} + + for key, value in sorted(CERT_DATA[name].items()): + # Handle cryptography extensions + if key == "extensions": + ctx["extensions"] = {ext["type"]: ext for ext in CERT_DATA[name].get("extensions", [])} + elif key == "precert_poison": + ctx["precert_poison"] = "* Precert Poison (critical):\n Yes" + elif isinstance(value, x509.Extension): + if value.critical: + ctx[f"{key}_critical"] = " (critical)" + else: + ctx[f"{key}_critical"] = "" + + ctx[f"{key}_text"] = textwrap.indent(extension_as_text(value.value), " ") + elif key == "path_length": + ctx[key] = value + ctx[f"{key}_text"] = "unlimited" if value is None else value + else: + ctx[key] = value + + if parent := CERT_DATA[name].get("parent"): + ctx["parent_name"] = CERT_DATA[parent]["name"] + ctx["parent_serial"] = CERT_DATA[parent]["serial"] + ctx["parent_serial_colons"] = CERT_DATA[parent]["serial_colons"] + + if CERT_DATA[name]["key_filename"] is not False: + storage = storages["django-ca"] + ctx["key_path"] = storage.path(CERT_DATA[name]["key_filename"]) + return ctx + + def get_idp( full_name: Optional[Iterable[x509.GeneralName]] = None, indirect_crl: bool = False, diff --git a/ca/django_ca/tests/commands/test_import_cert.py b/ca/django_ca/tests/commands/test_import_cert.py index c00246ebc..df0c4f6fb 100644 --- a/ca/django_ca/tests/commands/test_import_cert.py +++ b/ca/django_ca/tests/commands/test_import_cert.py @@ -15,9 +15,11 @@ from typing import Any +import pytest + from django_ca.models import Certificate, CertificateAuthority from django_ca.tests.base.assertions import assert_command_error, assert_signature -from django_ca.tests.base.constants import CERT_DATA +from django_ca.tests.base.constants import CERT_DATA, TIMESTAMPS from django_ca.tests.base.utils import cmd @@ -30,6 +32,7 @@ def import_cert(name: str, **kwargs: Any) -> Certificate: return Certificate.objects.get(serial=CERT_DATA["root-cert"]["serial"]) +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) def test_basic(root: CertificateAuthority) -> None: """Import a standard certificate.""" cert = import_cert("root-cert", ca=root) diff --git a/ca/django_ca/tests/commands/test_init_ca.py b/ca/django_ca/tests/commands/test_init_ca.py index 263895c3e..392d789eb 100644 --- a/ca/django_ca/tests/commands/test_init_ca.py +++ b/ca/django_ca/tests/commands/test_init_ca.py @@ -437,6 +437,7 @@ def test_add_extensions_with_non_default_critical(hostname: str, ca_name: str) - ) +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) def test_add_extensions_with_formatting( hostname: str, ca_name: str, usable_root: CertificateAuthority ) -> None: @@ -472,6 +473,7 @@ def test_add_extensions_with_formatting( ) +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) def test_add_extensions_with_formatting_without_uri( hostname: str, ca_name: str, usable_root: CertificateAuthority ) -> None: @@ -514,6 +516,7 @@ def test_add_extensions_with_formatting_without_uri( ) +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) def test_sign_extensions(hostname: str, ca_name: str, usable_root: CertificateAuthority) -> None: """Test adding extensions for signed certificates.""" ca = init_ca_e2e( @@ -829,6 +832,7 @@ def test_intermediate_check(ca_name: str) -> None: assert path_length_none_1.max_path_length == 1 +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) def test_expires_override(ca_name: str, usable_root: CertificateAuthority) -> None: """Test that if we request an expiry after that of the parent, we override to that of the parent.""" expires = usable_root.expires - timezone.now() + timedelta(days=10) @@ -846,6 +850,7 @@ def test_expires_override(ca_name: str, usable_root: CertificateAuthority) -> No assert_authority_key_identifier(usable_root, child) +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) def test_expires_override_with_use_tz_false( settings: SettingsWrapper, ca_name: str, usable_root: CertificateAuthority ) -> None: @@ -907,6 +912,7 @@ def test_password(ca_name: str, key_backend: StoragesBackend) -> None: assert_signature([parent], child) +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) def test_parent_password_with_ca_passwords( ca_name: str, usable_pwd: CertificateAuthority, settings: SettingsWrapper ) -> None: @@ -963,6 +969,7 @@ def test_no_default_hostname(ca_name: str) -> None: assert ca.sign_issuer_alternative_name is None +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) def test_multiple_ocsp_and_ca_issuers(hostname: str, ca_name: str, usable_root: CertificateAuthority) -> None: """Test using multiple OCSP responders and CA issuers.""" ocsp_uri_one = "http://ocsp.example.com/one" diff --git a/ca/django_ca/tests/commands/test_view_ca.py b/ca/django_ca/tests/commands/test_view_ca.py index 8c4427121..e49fdaadf 100644 --- a/ca/django_ca/tests/commands/test_view_ca.py +++ b/ca/django_ca/tests/commands/test_view_ca.py @@ -19,16 +19,22 @@ from cryptography import x509 -from django.conf import settings -from django.test import TestCase - -from freezegun import freeze_time +import pytest +from pytest_django.fixtures import SettingsWrapper +from django_ca.models import CertificateAuthority from django_ca.tests.base.constants import TIMESTAMPS -from django_ca.tests.base.mixins import TestCaseMixin -from django_ca.tests.base.utils import cmd, issuer_alternative_name, override_tmpcadir, uri +from django_ca.tests.base.utils import ( + certificate_policies, + cmd, + get_cert_context, + issuer_alternative_name, + uri, +) from django_ca.utils import format_general_name +pytestmark = [pytest.mark.freeze_time(TIMESTAMPS["everything_valid"])] + expected = { "ec": """* Name: {name} * Enabled: Yes @@ -241,6 +247,104 @@ * Valid until: {valid_until_str} * Status: Valid +Certificate Authority information: +* Certificate authority is a root CA. +* Certificate authority has no children. +* Maximum levels of sub-CAs (path length): {path_length_text} + +Key storage options: +* backend: default +* path: {key_filename} + +ACMEv2 support: +* Enabled: False + +Certificate extensions: +* Authority Key Identifier{authority_key_identifier_critical}: +{authority_key_identifier_text} +* Basic Constraints{basic_constraints_critical}: +{basic_constraints_text} +* Key Usage{key_usage_critical}: +{key_usage_text} +* Subject Key Identifier{subject_key_identifier_critical}: +{subject_key_identifier_text} + +Certificate extensions for signed certificates: +* Authority Information Access: + CA Issuers: + * URI:{sign_authority_information_access[value][1][access_location][value]} + OCSP: + * URI:{sign_authority_information_access[value][0][access_location][value]} +* CRL Distribution Points: + * DistributionPoint: + * Full Name: + * URI:{sign_crl_distribution_points[value][0][full_name][0][value]} + +Digest: + SHA-256: {sha256} + SHA-512: {sha512} + +{pub[pem]}""", + "root-no-key-backend-options": """* Name: {name} +* Enabled: Yes +* Subject: + * commonName (CN): {name}.example.com +* Serial: {serial_colons} +* Issuer: + * commonName (CN): {name}.example.com +* Valid from: {valid_from_str} +* Valid until: {valid_until_str} +* Status: Valid + +Certificate Authority information: +* Certificate authority is a root CA. +* Certificate authority has no children. +* Maximum levels of sub-CAs (path length): {path_length_text} + +Key storage options: +* backend: default +* No information available. + +ACMEv2 support: +* Enabled: False + +Certificate extensions: +* Authority Key Identifier{authority_key_identifier_critical}: +{authority_key_identifier_text} +* Basic Constraints{basic_constraints_critical}: +{basic_constraints_text} +* Key Usage{key_usage_critical}: +{key_usage_text} +* Subject Key Identifier{subject_key_identifier_critical}: +{subject_key_identifier_text} + +Certificate extensions for signed certificates: +* Authority Information Access: + CA Issuers: + * URI:{sign_authority_information_access[value][1][access_location][value]} + OCSP: + * URI:{sign_authority_information_access[value][0][access_location][value]} +* CRL Distribution Points: + * DistributionPoint: + * Full Name: + * URI:{sign_crl_distribution_points[value][0][full_name][0][value]} + +Digest: + SHA-256: {sha256} + SHA-512: {sha512} + +{pub[pem]}""", + "root-with-children": """* Name: {name} +* Enabled: Yes +* Subject: + * commonName (CN): {name}.example.com +* Serial: {serial_colons} +* Issuer: + * commonName (CN): {name}.example.com +* Valid from: {valid_from_str} +* Valid until: {valid_until_str} +* Status: Valid + Certificate Authority information: * Certificate authority is a root CA. * Children: @@ -293,8 +397,7 @@ Certificate Authority information: * Certificate authority is a root CA. -* Children: - * {children[0][0]} ({children[0][1]}) +* Certificate authority has no children. * Maximum levels of sub-CAs (path length): {path_length_text} Key storage options: @@ -333,8 +436,7 @@ Certificate Authority information: * Certificate authority is a root CA. -* Children: - * {children[0][0]} ({children[0][1]}) +* Certificate authority has no children. * Maximum levels of sub-CAs (path length): {path_length_text} Key storage options: @@ -372,8 +474,7 @@ Certificate Authority information: * Certificate authority is a root CA. -* Children: - * {children[0][0]} ({children[0][1]}) +* Certificate authority has no children. * Maximum levels of sub-CAs (path length): {path_length_text} Key storage options: @@ -426,8 +527,7 @@ Certificate Authority information: * Certificate authority is a root CA. -* Children: - * {children[0][0]} ({children[0][1]}) +* Certificate authority has no children. * Maximum levels of sub-CAs (path length): {path_length_text} Key storage options: @@ -473,8 +573,7 @@ Certificate Authority information: * Certificate authority is a root CA. -* Children: - * {children[0][0]} ({children[0][1]}) +* Certificate authority has no children. * Maximum levels of sub-CAs (path length): {path_length_text} Key storage options: @@ -513,6 +612,46 @@ * Issuer Alternative Name: * {sign_issuer_alternative_name} +Digest: + SHA-256: {sha256} + SHA-512: {sha512} + +{pub[pem]}""", + "root-no-sign-options": """* Name: {name} +* Enabled: Yes +* Subject: + * commonName (CN): {name}.example.com +* Serial: {serial_colons} +* Issuer: + * commonName (CN): {name}.example.com +* Valid from: {valid_from_str} +* Valid until: {valid_until_str} +* Status: Valid + +Certificate Authority information: +* Certificate authority is a root CA. +* Certificate authority has no children. +* Maximum levels of sub-CAs (path length): {path_length_text} + +Key storage options: +* backend: default +* path: {key_filename} + +ACMEv2 support: +* Enabled: False + +Certificate extensions: +* Authority Key Identifier{authority_key_identifier_critical}: +{authority_key_identifier_text} +* Basic Constraints{basic_constraints_critical}: +{basic_constraints_text} +* Key Usage{key_usage_critical}: +{key_usage_text} +* Subject Key Identifier{subject_key_identifier_critical}: +{subject_key_identifier_text} + +No certificate extensions for signed certificates. + Digest: SHA-256: {sha256} SHA-512: {sha512} @@ -531,8 +670,7 @@ Certificate Authority information: * Certificate authority is a root CA. -* Children: - * {children[0][0]} ({children[0][1]}) +* Certificate authority has no children. * Maximum levels of sub-CAs (path length): {path_length_text} Key storage options: @@ -585,7 +723,7 @@ Key storage options: * backend: default -* No information available. +* path: globalsign.key ACMEv2 support: * Enabled: False @@ -629,7 +767,7 @@ Key storage options: * backend: default -* No information available. +* path: digicert_ev_root.key ACMEv2 support: * Enabled: False @@ -677,7 +815,7 @@ Key storage options: * backend: default -* No information available. +* path: comodo.key ACMEv2 support: * Enabled: False @@ -719,7 +857,7 @@ Key storage options: * backend: default -* No information available. +* path: identrust_root_1.key ACMEv2 support: * Enabled: False @@ -761,7 +899,7 @@ Key storage options: * backend: default -* No information available. +* path: globalsign_r2_root.key ACMEv2 support: * Enabled: False @@ -811,7 +949,7 @@ Key storage options: * backend: default -* No information available. +* path: comodo_dv.key ACMEv2 support: * Enabled: False @@ -863,7 +1001,7 @@ Key storage options: * backend: default -* No information available. +* path: rapidssl_g3.key ACMEv2 support: * Enabled: False @@ -913,7 +1051,7 @@ Key storage options: * backend: default -* No information available. +* path: geotrust.key ACMEv2 support: * Enabled: False @@ -959,7 +1097,7 @@ Key storage options: * backend: default -* No information available. +* path: comodo_ev.key ACMEv2 support: * Enabled: False @@ -1011,7 +1149,7 @@ Key storage options: * backend: default -* No information available. +* path: digicert_ha_intermediate.key ACMEv2 support: * Enabled: False @@ -1061,7 +1199,7 @@ Key storage options: * backend: default -* No information available. +* path: dst_root_x3.key ACMEv2 support: * Enabled: False @@ -1104,7 +1242,7 @@ Key storage options: * backend: default -* No information available. +* path: globalsign_dv.key ACMEv2 support: * Enabled: False @@ -1159,7 +1297,7 @@ Key storage options: * backend: default -* No information available. +* path: godaddy_g2_intermediate.key ACMEv2 support: * Enabled: False @@ -1213,7 +1351,7 @@ Key storage options: * backend: default -* No information available. +* path: godaddy_g2_root.key ACMEv2 support: * Enabled: False @@ -1255,7 +1393,7 @@ Key storage options: * backend: default -* No information available. +* path: google_g3.key ACMEv2 support: * Enabled: False @@ -1306,7 +1444,7 @@ Key storage options: * backend: default -* No information available. +* path: letsencrypt_x1.key ACMEv2 support: * Enabled: False @@ -1357,7 +1495,7 @@ Key storage options: * backend: default -* No information available. +* path: letsencrypt_x3.key ACMEv2 support: * Enabled: False @@ -1409,7 +1547,7 @@ Key storage options: * backend: default -* No information available. +* path: startssl_root.key ACMEv2 support: * Enabled: False @@ -1461,7 +1599,7 @@ Key storage options: * backend: default -* No information available. +* path: startssl_class2.key ACMEv2 support: * Enabled: False @@ -1513,7 +1651,7 @@ Key storage options: * backend: default -* No information available. +* path: startssl_class3.key ACMEv2 support: * Enabled: False @@ -1566,7 +1704,7 @@ Key storage options: * backend: default -* No information available. +* path: trustid_server_a52.key ACMEv2 support: * Enabled: False @@ -1620,7 +1758,7 @@ Key storage options: * backend: default -* No information available. +* path: digicert_global_root.key ACMEv2 support: * Enabled: False @@ -1665,7 +1803,7 @@ Key storage options: * backend: default -* No information available. +* path: digicert_sha2.key ACMEv2 support: * Enabled: False @@ -1700,160 +1838,165 @@ expected["pwd"] = expected["ec"] -class ViewCATestCase(TestCaseMixin, TestCase): - """Main test class for this command.""" - - load_cas = "__all__" - - def _wrap_hash(self, text: str, columns: int) -> str: - return "\n".join(textwrap.wrap(text, columns, subsequent_indent=" " * 11)) - - @override_tmpcadir() - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_all_cas(self) -> None: - """Test viewing all CAs.""" - for name, ca in sorted(self.cas.items(), key=lambda t: t[0]): - stdout, stderr = cmd("view_ca", ca.serial, wrap=False) - data = self.get_cert_context(name) - self.assertMultiLineEqual(stdout, expected[name].format(**data), name) - assert stderr == "" - - @override_tmpcadir(USE_TZ=True) - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_with_timezone_support(self) -> None: - """Test viewing certificate with USE_TZ=True.""" - self.assertTrue(settings.USE_TZ) - - stdout, stderr = cmd("view_ca", self.ca.serial, wrap=False) - data = self.get_cert_context(self.ca.name) - self.assertMultiLineEqual(stdout, expected[self.ca.name].format(**data), self.ca.name) - self.assertEqual(stderr, "") - - @override_tmpcadir(USE_TZ=False) - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_with_use_tz_is_false(self) -> None: - """Test viewing certificate without timezone support.""" - self.assertFalse(settings.USE_TZ) - - stdout, stderr = cmd("view_ca", self.ca.serial, wrap=False) - data = self.get_cert_context(self.ca.name) - self.assertMultiLineEqual(stdout, expected[self.ca.name].format(**data), self.ca.name) - self.assertEqual(stderr, "") - - @override_tmpcadir() - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_properties(self) -> None: - """Test viewing of various optional properties.""" - ca = self.cas["root"] - hostname = "ca.example.com" - ca.website = f"https://website.{hostname}" - ca.terms_of_service = f"{ca.website}/tos/" - ca.caa_identity = hostname - ca.acme_enabled = True - ca.acme_requires_contact = False - ca.save() - - stdout, stderr = cmd("view_ca", ca.serial, wrap=False) - self.assertEqual(stderr, "") - data = self.get_cert_context("root") - self.assertMultiLineEqual(stdout, expected["root-properties"].format(ca=ca, **data)) - - @override_tmpcadir(CA_ENABLE_ACME=False) - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_acme_disabled(self) -> None: - """Test viewing when ACME is disabled.""" - stdout, stderr = cmd("view_ca", self.cas["root"].serial, wrap=False) - self.assertEqual(stderr, "") - data = self.get_cert_context("root") - self.assertMultiLineEqual(stdout, expected["root-acme-disabled"].format(**data)) - - @override_tmpcadir() - def test_no_extensions(self) -> None: - """Test viewing a CA without extensions.""" - stdout, stderr = cmd("view_ca", self.cas["root"].serial, extensions=False, wrap=False) - self.assertEqual(stderr, "") - data = self.get_cert_context("root") - self.assertMultiLineEqual(stdout, expected["root-no-extensions"].format(**data)) - - @override_tmpcadir() - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_sign_options(self) -> None: - """Test options for signing certificates.""" - ian_uri = uri("http://ian.example.com") - ca = self.cas["root"] - self.assertIsNotNone(ca.sign_authority_information_access) - ca.sign_certificate_policies = self.certificate_policies( - x509.PolicyInformation( - policy_identifier=x509.ObjectIdentifier("1.2.3"), - policy_qualifiers=[ - "https://cps.example.com", - x509.UserNotice(notice_reference=None, explicit_text="explicit-text"), - ], - ) +def _wrap_hash(text: str, columns: int) -> str: + return "\n".join(textwrap.wrap(text, columns, subsequent_indent=" " * 11)) + + +def test_all_cas(ca: CertificateAuthority) -> None: + """Test viewing all CAs.""" + stdout, stderr = cmd("view_ca", ca.serial, wrap=False) + data = get_cert_context(ca.name) + assert stderr == "" + assert stdout == expected[ca.name].format(**data), ca.name + + +@pytest.mark.usefixtures("child") # child fixture sets parent, so root has children +def test_with_children(usable_root: CertificateAuthority) -> None: + """Test viewing a CA with children.""" + stdout, stderr = cmd("view_ca", usable_root.serial, wrap=False) + assert stderr == "" + data = get_cert_context("root") + assert stdout == expected["root-with-children"].format(ca=usable_root, **data) + + +def test_no_key_backend_options(usable_root: CertificateAuthority) -> None: + """Test viewing a CA with children.""" + usable_root.key_backend_options = {} + usable_root.save() + stdout, stderr = cmd("view_ca", usable_root.serial, wrap=False) + assert stderr == "" + data = get_cert_context("root") + assert stdout == expected["root-no-key-backend-options"].format(ca=usable_root, **data) + + +def test_without_timezone_support(usable_child: CertificateAuthority, settings: SettingsWrapper) -> None: + """Test viewing certificate with USE_TZ=False.""" + settings.USE_TZ = False + + stdout, stderr = cmd("view_ca", usable_child.serial, wrap=False) + assert stderr == "" + data = get_cert_context(usable_child.name) + assert stdout == expected[usable_child.name].format(**data) + + +def test_properties(usable_root: CertificateAuthority, hostname: str) -> None: + """Test viewing of various optional properties.""" + usable_root.website = f"https://website.{hostname}" + usable_root.terms_of_service = f"{usable_root.website}/tos/" + usable_root.caa_identity = hostname + usable_root.acme_enabled = True + usable_root.acme_requires_contact = False + usable_root.save() + + stdout, stderr = cmd("view_ca", usable_root.serial, wrap=False) + assert stderr == "" + data = get_cert_context("root") + assert stdout == expected["root-properties"].format(ca=usable_root, **data) + + +def test_acme_disabled(usable_root: CertificateAuthority, settings: SettingsWrapper) -> None: + """Test viewing when ACME is disabled.""" + settings.CA_ENABLE_ACME = False + stdout, stderr = cmd("view_ca", usable_root.serial, wrap=False) + assert stderr == "" + data = get_cert_context("root") + assert stdout == expected["root-acme-disabled"].format(**data) + + +def test_no_extensions(usable_root: CertificateAuthority) -> None: + """Test viewing a CA without extensions.""" + stdout, stderr = cmd("view_ca", usable_root.serial, extensions=False, wrap=False) + assert stderr == "" + data = get_cert_context("root") + assert stdout == expected["root-no-extensions"].format(**data) + + +def test_sign_options(usable_root: CertificateAuthority) -> None: + """Test options for signing certificates.""" + ian_uri = uri("http://ian.example.com") + assert usable_root.sign_authority_information_access is not None + usable_root.sign_certificate_policies = certificate_policies( + x509.PolicyInformation( + policy_identifier=x509.ObjectIdentifier("1.2.3"), + policy_qualifiers=[ + "https://cps.example.com", + x509.UserNotice(notice_reference=None, explicit_text="explicit-text"), + ], ) - self.assertIsNotNone(ca.sign_crl_distribution_points) - ca.sign_issuer_alternative_name = issuer_alternative_name(ian_uri) - ca.save() - - stdout, stderr = cmd("view_ca", ca.serial, wrap=False) - self.assertEqual(stderr, "") - data = self.get_cert_context("root") - data["sign_issuer_alternative_name"] = format_general_name(ian_uri) - self.assertMultiLineEqual(stdout, expected["root-sign-options"].format(**data)) - - @override_tmpcadir() - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_sign_options_only_issuer_alternative_name(self) -> None: - """Test options for signing certificates with only an Issuer Alternative Name. - - This is necessary to check full branch coverage, otherwise Authority Information Access and CRL - Distribution Points is always set. - """ - ian_uri = uri("http://ian.example.com") - ca = self.cas["root"] - ca.sign_authority_information_access = None - ca.sign_certificate_policies = None - ca.sign_crl_distribution_points = None - ca.sign_issuer_alternative_name = issuer_alternative_name(ian_uri) - ca.save() - - stdout, stderr = cmd("view_ca", ca.serial, wrap=False) - self.assertEqual(stderr, "") - data = self.get_cert_context("root") - data["sign_issuer_alternative_name"] = format_general_name(ian_uri) - self.assertMultiLineEqual( - stdout, expected["root-sign-options-only-issuer-alternative-name"].format(**data) - ) - - def test_wrap_digest(self) -> None: - """Test wrapping the digest.""" - data = self.get_cert_context("root") - sha256 = data["sha256"] - sha512 = data["sha512"] - - with mock.patch("shutil.get_terminal_size", return_value=os.terminal_size((64, 0))) as shutil_mock: - stdout, stderr = cmd("view_ca", self.cas["root"].serial, pem=False, extensions=False) - - # Django calls get_terminal_size as well, so the number of calls is unpredictable - shutil_mock.assert_called_with(fallback=(107, 100)) - self.assertEqual(stderr, "") - - data["sha256"] = self._wrap_hash(f" SHA-256: {sha256}", 62) - data["sha512"] = self._wrap_hash(f" SHA-512: {sha512}", 62) - self.assertMultiLineEqual(stdout, expected["root-no-wrap"].format(**data)) - - # try with decreasing terminal size - with mock.patch("shutil.get_terminal_size", return_value=os.terminal_size((63, 0))) as shutil_mock: - stdout, stderr = cmd("view_ca", self.cas["root"].serial, pem=False, extensions=False) - self.assertMultiLineEqual(stdout, expected["root-no-wrap"].format(**data)) - - with mock.patch("shutil.get_terminal_size", return_value=os.terminal_size((62, 0))) as shutil_mock: - stdout, stderr = cmd("view_ca", self.cas["root"].serial, pem=False, extensions=False) - self.assertMultiLineEqual(stdout, expected["root-no-wrap"].format(**data)) - - # Get smaller, so we wrap another element in the colon'd hash - with mock.patch("shutil.get_terminal_size", return_value=os.terminal_size((61, 0))) as shutil_mock: - stdout, stderr = cmd("view_ca", self.cas["root"].serial, pem=False, extensions=False) - data["sha256"] = self._wrap_hash(f" SHA-256: {sha256}", 59) - data["sha512"] = self._wrap_hash(f" SHA-512: {sha512}", 59) - self.assertMultiLineEqual(stdout, expected["root-no-wrap"].format(**data)) + ) + assert usable_root.sign_crl_distribution_points is not None + usable_root.sign_issuer_alternative_name = issuer_alternative_name(ian_uri) + usable_root.save() + + stdout, stderr = cmd("view_ca", usable_root.serial, wrap=False) + assert stderr == "" + data = get_cert_context("root") + data["sign_issuer_alternative_name"] = format_general_name(ian_uri) + assert stdout == expected["root-sign-options"].format(**data) + + +def test_no_sign_options(usable_root: CertificateAuthority) -> None: + """Test viewing a CA with no signing options.""" + usable_root.sign_authority_information_access = None + usable_root.sign_certificate_policies = None + usable_root.sign_crl_distribution_points = None + usable_root.sign_issuer_alternative_name = None + usable_root.save() + stdout, stderr = cmd("view_ca", usable_root.serial, wrap=False) + assert stderr == "" + data = get_cert_context("root") + assert stdout == expected["root-no-sign-options"].format(ca=usable_root, **data) + + +def test_sign_options_only_issuer_alternative_name(usable_root: CertificateAuthority) -> None: + """Test options for signing certificates with only an Issuer Alternative Name. + + This is necessary to check full branch coverage, otherwise Authority Information Access and CRL + Distribution Points is always set. + """ + ian_uri = uri("http://ian.example.com") + usable_root.sign_authority_information_access = None + usable_root.sign_certificate_policies = None + usable_root.sign_crl_distribution_points = None + usable_root.sign_issuer_alternative_name = issuer_alternative_name(ian_uri) + usable_root.save() + + stdout, stderr = cmd("view_ca", usable_root.serial, wrap=False) + assert stderr == "" + data = get_cert_context("root") + data["sign_issuer_alternative_name"] = format_general_name(ian_uri) + assert stdout == expected["root-sign-options-only-issuer-alternative-name"].format(**data) + + +def test_wrap_digest(usable_root: CertificateAuthority) -> None: + """Test wrapping the digest.""" + data = get_cert_context("root") + sha256 = data["sha256"] + sha512 = data["sha512"] + + with mock.patch("shutil.get_terminal_size", return_value=os.terminal_size((64, 0))) as shutil_mock: + stdout, stderr = cmd("view_ca", usable_root.serial, pem=False, extensions=False) + + # Django calls get_terminal_size as well, so the number of calls is unpredictable + shutil_mock.assert_called_with(fallback=(107, 100)) + assert stderr == "" + + data["sha256"] = _wrap_hash(f" SHA-256: {sha256}", 62) + data["sha512"] = _wrap_hash(f" SHA-512: {sha512}", 62) + assert stdout == expected["root-no-wrap"].format(**data) + + # try with decreasing terminal size + with mock.patch("shutil.get_terminal_size", return_value=os.terminal_size((63, 0))) as shutil_mock: + stdout, stderr = cmd("view_ca", usable_root.serial, pem=False, extensions=False) + assert stdout == expected["root-no-wrap"].format(**data) + + with mock.patch("shutil.get_terminal_size", return_value=os.terminal_size((62, 0))) as shutil_mock: + stdout, stderr = cmd("view_ca", usable_root.serial, pem=False, extensions=False) + assert stdout == expected["root-no-wrap"].format(**data) + + # Get smaller, so we wrap another element in the colon'd hash + with mock.patch("shutil.get_terminal_size", return_value=os.terminal_size((61, 0))) as shutil_mock: + stdout, stderr = cmd("view_ca", usable_root.serial, pem=False, extensions=False) + data["sha256"] = _wrap_hash(f" SHA-256: {sha256}", 59) + data["sha512"] = _wrap_hash(f" SHA-512: {sha512}", 59) + assert stdout == expected["root-no-wrap"].format(**data) diff --git a/ca/django_ca/tests/commands/test_view_cert.py b/ca/django_ca/tests/commands/test_view_cert.py index 4b592d627..19fe68607 100644 --- a/ca/django_ca/tests/commands/test_view_cert.py +++ b/ca/django_ca/tests/commands/test_view_cert.py @@ -19,15 +19,13 @@ from cryptography import x509 from cryptography.x509.oid import ExtensionOID -from django.test import TestCase, override_settings - +import pytest from freezegun import freeze_time -from django_ca.models import Watcher +from django_ca.models import Certificate, Watcher from django_ca.tests.base.assertions import assert_command_error from django_ca.tests.base.constants import CERT_DATA, TIMESTAMPS -from django_ca.tests.base.mixins import TestCaseMixin -from django_ca.tests.base.utils import cmd, override_tmpcadir +from django_ca.tests.base.utils import cmd, get_cert_context expected = { "root-cert": """* Subject: @@ -603,26 +601,19 @@ } -@override_settings(CA_MIN_KEY_SIZE=1024, CA_PROFILES={}, CA_DEFAULT_SUBJECT=tuple()) -class ViewCertTestCase(TestCaseMixin, TestCase): - """Main test class for this command.""" - - load_cas = "__all__" - load_certs = "__all__" - - def assertBasicOutput(self, status: str) -> None: # pylint: disable=invalid-name - """Test basic properties of output.""" - # pylint: disable=consider-using-f-string - for key, cert in self.ca_certs: - stdout, stderr = cmd("view_cert", cert.serial, wrap=False) - san = typing.cast( - Optional[x509.Extension[x509.SubjectAlternativeName]], - cert.extensions.get(ExtensionOID.SUBJECT_ALTERNATIVE_NAME), - ) - if san is None: - self.assertEqual( - stdout, - """Common Name: {cn} +def assert_basic_output(name: str, cert: Certificate, status: str) -> None: + """Test basic properties of output.""" + # pylint: disable=consider-using-f-string + + stdout, stderr = cmd("view_cert", cert.serial, wrap=False) + san = typing.cast( + Optional[x509.Extension[x509.SubjectAlternativeName]], + cert.extensions.get(ExtensionOID.SUBJECT_ALTERNATIVE_NAME), + ) + if san is None: + assert ( + stdout + == """Common Name: {cn} Valid from: {valid_from_str} Valid until: {valid_until_str} Status: {status} @@ -631,14 +622,15 @@ def assertBasicOutput(self, status: str) -> None: # pylint: disable=invalid-nam SHA-256: {sha256} SHA-512: {sha512} -{pub[pem]}""".format(status=status, **self.get_cert_context(key)), - ) - elif len(san.value) != 1: - continue # no need to duplicate this here - else: - self.assertEqual( - stdout, - """* Subject: +{pub[pem]}""".format(status=status, **get_cert_context(name)) + ) + + elif len(san.value) != 1: + pass + else: + assert ( + stdout + == """* Subject: * countryName (C): AT * stateOrProvinceName (ST): Vienna * localityName (L): Vienna @@ -675,16 +667,15 @@ def assertBasicOutput(self, status: str) -> None: # pylint: disable=invalid-nam SHA-256: {sha256} SHA-512: {sha512} -{pub[pem]}""".format(status=status, **self.get_cert_context(key)), - ) - self.assertEqual(stderr, "") +{pub[pem]}""".format(status=status, **get_cert_context(name)) + ) + assert stderr == "" - # test with no pem and no extensions - for key, cert in self.ca_certs: - stdout, stderr = cmd("view_cert", cert.serial, pem=False, extensions=False, wrap=False) - self.assertEqual( - stdout, - """* Subject: + # test with no pem and no extensions + stdout, stderr = cmd("view_cert", cert.serial, pem=False, extensions=False, wrap=False) + assert ( + stdout + == """* Subject: * countryName (C): AT * stateOrProvinceName (ST): Vienna * localityName (L): Vienna @@ -702,39 +693,52 @@ def assertBasicOutput(self, status: str) -> None: # pylint: disable=invalid-nam Digest: SHA-256: {sha256} SHA-512: {sha512} -""".format(status=status, **self.get_cert_context(key)), - ) - self.assertEqual(stderr, "") - - @freeze_time(TIMESTAMPS["before_everything"]) - def test_basic_not_yet_valid(self) -> None: - """Basic tests when all certs are not yet valid.""" - self.assertBasicOutput(status="Not yet valid") - - @freeze_time(TIMESTAMPS["everything_expired"]) - def test_basic_expired(self) -> None: - """Basic tests when all certs are expired.""" - self.assertBasicOutput(status="Expired") - - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_certs(self) -> None: - """Test main certs.""" - for name, cert in self.usable_certs: - stdout, stderr = cmd("view_cert", cert.serial, pem=False, extensions=True, wrap=False) - self.assertEqual(stderr, "") - - context = self.get_cert_context(name) - self.assertEqual(stdout, expected[name].format(**context), name) - - def test_revoked(self) -> None: - """Test viewing a revoked cert.""" - # pylint: disable=consider-using-f-string - self.cert.revoked = True - self.cert.save() - stdout, stderr = cmd("view_cert", self.cert.serial, pem=False, wrap=False, extensions=False) - self.assertEqual( - stdout, - """* Subject: +""".format(status=status, **get_cert_context(name)) + ) + assert stderr == "" + + +def assert_contrib(name: str, exp: str, **context: str) -> None: + """Assert basic contrib output.""" + serial = CERT_DATA[name]["serial"] + stdout, stderr = cmd("view_cert", serial, pem=False, extensions=True, wrap=False) + context.update(get_cert_context(name)) + assert stderr == "" + assert stdout == exp.format(**context) + + +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) +def test_certs(usable_cert: Certificate) -> None: + """Test main certs.""" + stdout, stderr = cmd("view_cert", usable_cert.serial, pem=False, extensions=True, wrap=False) + assert stderr == "" + + name = usable_cert.test_name # type: ignore[attr-defined] + context = get_cert_context(name) + assert stdout == expected[name].format(**context), name + + +@pytest.mark.freeze_time(TIMESTAMPS["before_everything"]) +def test_basic_not_yet_valid(root_cert: Certificate) -> None: + """Basic tests when all certs are not yet valid.""" + assert_basic_output("root-cert", root_cert, status="Not yet valid") + + +@pytest.mark.freeze_time(TIMESTAMPS["everything_expired"]) +def test_basic_expired(root_cert: Certificate) -> None: + """Basic tests when all certs are expired.""" + assert_basic_output("root-cert", root_cert, status="Expired") + + +def test_revoked(child_cert: Certificate) -> None: + """Test viewing a revoked cert.""" + # pylint: disable=consider-using-f-string + child_cert.revoke() + stdout, stderr = cmd("view_cert", child_cert.serial, pem=False, wrap=False, extensions=False) + assert stderr == "" + assert ( + stdout + == """* Subject: * countryName (C): AT * stateOrProvinceName (ST): Vienna * localityName (L): Vienna @@ -752,23 +756,22 @@ def test_revoked(self) -> None: Digest: SHA-256: {sha256} SHA-512: {sha512} -""".format(**CERT_DATA["child-cert"]), - ) - self.assertEqual(stderr, "") - - @freeze_time(TIMESTAMPS["everything_valid"]) - @override_tmpcadir() - def test_no_san_with_watchers(self) -> None: - """Test a cert with no subjectAltNames but with watchers.""" - cert = self.certs["no-extensions"] - watcher = Watcher.from_addr("user@example.com") - cert.watchers.add(watcher) - - stdout, stderr = cmd("view_cert", cert.serial, pem=False, extensions=False, wrap=False) - self.assertEqual( - stdout, - # pylint: disable-next=consider-using-f-string - """* Subject: +""".format(**CERT_DATA["child-cert"]) + ) + + +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) +def test_no_san_with_watchers(no_extensions: Certificate) -> None: + """Test a cert with no subjectAltNames but with watchers.""" + # pylint: disable=consider-using-f-string + watcher = Watcher.from_addr("user@example.com") + no_extensions.watchers.add(watcher) + + stdout, stderr = cmd("view_cert", no_extensions.serial, pem=False, extensions=False, wrap=False) + assert stderr == "" + assert ( + stdout + == """* Subject: * commonName (CN): {name}.example.com * Serial: {serial_colons} * Issuer: @@ -782,31 +785,18 @@ def test_no_san_with_watchers(self) -> None: Digest: SHA-256: {sha256} SHA-512: {sha512} -""".format(**self.get_cert_context("no-extensions")), - ) - self.assertEqual(stderr, "") - - def assertContrib(self, name: str, exp: str, **context: str) -> None: # pylint: disable=invalid-name - """Assert basic contrib output.""" - cert = self.certs[name] - stdout, stderr = cmd("view_cert", cert.serial, pem=False, extensions=True, wrap=False) - context.update(self.get_cert_context(name)) - self.assertEqual(stderr, "") - self.assertEqual(stdout, exp.format(**context)) - - @freeze_time("2019-04-01") - def test_contrib_godaddy_derstandardat(self) -> None: - """Test contrib godaddy cert for derstandard.at.""" - id1 = ( - "A4:B9:09:90:B4:18:58:14:87:BB:13:A2:CC:67:70:0A:3C:35:98:04:F9:1B:DF:B8:E3:77:CD:0E:C8:0D:DC:10" - ) - id2 = ( - "EE:4B:BD:B7:75:CE:60:BA:E1:42:69:1F:AB:E1:9E:66:A3:0F:7E:5F:B0:72:D8:83:00:C4:7B:89:7A:A8:FD:CB" - ) - id3 = ( - "44:94:65:2E:B0:EE:CE:AF:C4:40:07:D8:A8:FE:28:C0:DA:E6:82:BE:D8:CB:31:B5:3F:D3:33:96:B5:B6:81:A8" - ) - sct = f"""* Precertificate Signed Certificate Timestamps: +""".format(**get_cert_context("no-extensions")) + ) + + +@pytest.mark.freeze_time("2019-04-01") +@pytest.mark.usefixtures("contrib_godaddy_g2_intermediate_cert") +def test_contrib_godaddy_derstandardat() -> None: + """Test contrib godaddy cert for derstandard.at.""" + id1 = "A4:B9:09:90:B4:18:58:14:87:BB:13:A2:CC:67:70:0A:3C:35:98:04:F9:1B:DF:B8:E3:77:CD:0E:C8:0D:DC:10" + id2 = "EE:4B:BD:B7:75:CE:60:BA:E1:42:69:1F:AB:E1:9E:66:A3:0F:7E:5F:B0:72:D8:83:00:C4:7B:89:7A:A8:FD:CB" + id3 = "44:94:65:2E:B0:EE:CE:AF:C4:40:07:D8:A8:FE:28:C0:DA:E6:82:BE:D8:CB:31:B5:3F:D3:33:96:B5:B6:81:A8" + sct = f"""* Precertificate Signed Certificate Timestamps: * Precertificate (v1): Timestamp: 2019-03-27 09:13:54.342000 Log ID: {id1} @@ -817,9 +807,9 @@ def test_contrib_godaddy_derstandardat(self) -> None: Timestamp: 2019-03-27 09:13:56.485000 Log ID: {id3}""" - self.assertContrib( - "godaddy_g2_intermediate-cert", - """* Subject: + assert_contrib( + "godaddy_g2_intermediate-cert", + """* Subject: * organizationalUnitName (OU): Domain Control Validated * commonName (CN): derstandard.at * Serial: {serial_colons} @@ -860,23 +850,25 @@ def test_contrib_godaddy_derstandardat(self) -> None: SHA-256: {sha256} SHA-512: {sha512} """, - sct=sct, - ) - - @freeze_time("2019-07-05") - def test_contrib_letsencrypt_jabber_at(self) -> None: - """Test contrib letsencrypt cert.""" - # pylint: disable=consider-using-f-string - name = "letsencrypt_x3-cert" - context = self.get_cert_context(name) - context["id1"] = ( - "6F:53:76:AC:31:F0:31:19:D8:99:00:A4:51:15:FF:77:15:1C:11:D9:02:C1:00:29:06:8D:B2:08:9A:37:D9:13" - ) - context["id2"] = ( - "29:3C:51:96:54:C8:39:65:BA:AA:50:FC:58:07:D4:B7:6F:BF:58:7A:29:72:DC:A4:C3:0C:F4:E5:45:47:F4:78" - ) - - sct = """* Precertificate Signed Certificate Timestamps: + sct=sct, + ) + + +@pytest.mark.freeze_time("2019-07-05") +@pytest.mark.usefixtures("contrib_letsencrypt_x3_cert") +def test_contrib_letsencrypt_jabber_at() -> None: + """Test contrib letsencrypt cert.""" + # pylint: disable=consider-using-f-string + name = "letsencrypt_x3-cert" + context = get_cert_context(name) + context["id1"] = ( + "6F:53:76:AC:31:F0:31:19:D8:99:00:A4:51:15:FF:77:15:1C:11:D9:02:C1:00:29:06:8D:B2:08:9A:37:D9:13" + ) + context["id2"] = ( + "29:3C:51:96:54:C8:39:65:BA:AA:50:FC:58:07:D4:B7:6F:BF:58:7A:29:72:DC:A4:C3:0C:F4:E5:45:47:F4:78" + ) + + sct = """* Precertificate Signed Certificate Timestamps: * Precertificate (v1): Timestamp: 2019-06-25 03:40:03.920000 Log ID: {id1} @@ -884,9 +876,9 @@ def test_contrib_letsencrypt_jabber_at(self) -> None: Timestamp: 2019-06-25 03:40:03.862000 Log ID: {id2}""".format(**context) - self.assertContrib( - "letsencrypt_x3-cert", - """* Subject: + assert_contrib( + "letsencrypt_x3-cert", + """* Subject: * commonName (CN): jabber.at * Serial: {serial_colons} * Issuer: @@ -921,16 +913,18 @@ def test_contrib_letsencrypt_jabber_at(self) -> None: SHA-256: {sha256} SHA-512: {sha512} """, - sct=sct, - ) - - @freeze_time("2018-12-01") - def test_contrib_cloudflare_1(self) -> None: - """Test contrib cloudflare cert.""" - # pylint: disable=consider-using-f-string - self.assertContrib( - "cloudflare_1", - """* Subject: + sct=sct, + ) + + +@freeze_time("2018-12-01") +@pytest.mark.usefixtures("contrib_cloudflare_1") +def test_contrib_cloudflare_1() -> None: + """Test contrib cloudflare cert.""" + # pylint: disable=consider-using-f-string + assert_contrib( + "cloudflare_1", + """* Subject: * organizationalUnitName (OU): Domain Control Validated * organizationalUnitName (OU): PositiveSSL Multi-Domain * commonName (CN): sni24142.cloudflaressl.com @@ -970,14 +964,17 @@ def test_contrib_cloudflare_1(self) -> None: Digest: SHA-256: {sha256} SHA-512: {sha512} -""".format(**self.get_cert_context("cloudflare_1")), - ) +""".format(**get_cert_context("cloudflare_1")), + ) + - def test_contrib_multiple_ous(self) -> None: - """Test special contrib case with multiple OUs.""" - self.assertContrib( - "multiple_ous", - """* Subject: +@pytest.mark.freeze_time("2024-04-21") +@pytest.mark.usefixtures("contrib_multiple_ous") +def test_contrib_multiple_ous() -> None: + """Test special contrib case with multiple OUs.""" + assert_contrib( + "multiple_ous", + """* Subject: * countryName (C): US * organizationName (O): VeriSign, Inc. * organizationalUnitName (OU): Class 3 Public Primary Certification Authority - G2 @@ -1001,10 +998,12 @@ def test_contrib_multiple_ous(self) -> None: SHA-256: {sha256} SHA-512: {sha512} """, - ) + ) + - def test_unknown_cert(self) -> None: - """Test viewing an unknown certificate.""" - name = "foobar" - with assert_command_error(rf"^Error: argument cert: {name}: Certificate not found\.$"): - cmd("view_cert", name) +@pytest.mark.django_db +def test_unknown_cert() -> None: + """Test viewing an unknown certificate.""" + name = "foobar" + with assert_command_error(rf"^Error: argument cert: {name}: Certificate not found\.$"): + cmd("view_cert", name) diff --git a/ca/django_ca/tests/conftest.py b/ca/django_ca/tests/conftest.py index 0bf5c104f..e45e2cadb 100644 --- a/ca/django_ca/tests/conftest.py +++ b/ca/django_ca/tests/conftest.py @@ -33,13 +33,14 @@ from ca import settings_utils # noqa: F401 # to get rid of pytest warnings for untested modules from django_ca.tests.base.conftest_helpers import ( + contrib_ca_names, + contrib_cert_names, generate_ca_fixture, generate_cert_fixture, generate_pub_fixture, generate_usable_ca_fixture, interesting_certificate_names, setup_pragmas, - unusable_cert_names, usable_ca_names, usable_cert_names, ) @@ -144,7 +145,14 @@ def user_client(user: "User", client: Client) -> Iterator[Client]: for _ca_name in usable_ca_names: globals()[_ca_name] = generate_ca_fixture(_ca_name) globals()[f"usable_{_ca_name}"] = generate_usable_ca_fixture(_ca_name) -for _ca_name in usable_ca_names + usable_cert_names + unusable_cert_names: +for _ca_name in contrib_ca_names: + globals()[f"contrib_{_ca_name}"] = generate_ca_fixture(_ca_name) +for _ca_name in usable_ca_names + usable_cert_names: globals()[f"{_ca_name.replace('-', '_')}_pub"] = generate_pub_fixture(_ca_name) +for _ca_name in contrib_ca_names + contrib_cert_names: + globals()[f"contrib_{_ca_name.replace('-', '_')}_pub"] = generate_pub_fixture(_ca_name) for cert_name in usable_cert_names: globals()[cert_name.replace("-", "_")] = generate_cert_fixture(cert_name) +for cert_name in contrib_cert_names: + # raise Exception(contrib_cert_names, cert_name.replace("-", "_")) + globals()[f"contrib_{cert_name.replace('-', '_')}"] = generate_cert_fixture(cert_name) diff --git a/ca/django_ca/tests/fixtures/contrib/multiple_ous_and_no_ext.pub b/ca/django_ca/tests/fixtures/contrib/multiple_ous.pub similarity index 100% rename from ca/django_ca/tests/fixtures/contrib/multiple_ous_and_no_ext.pub rename to ca/django_ca/tests/fixtures/contrib/multiple_ous.pub diff --git a/ca/django_ca/tests/test_admin_acme.py b/ca/django_ca/tests/test_admin_acme.py index 6aac441f7..c94ac7184 100644 --- a/ca/django_ca/tests/test_admin_acme.py +++ b/ca/django_ca/tests/test_admin_acme.py @@ -14,10 +14,13 @@ """Test cases for ModelAdmin classes for ACME models.""" import typing +from datetime import timedelta from django.test import TestCase from django.utils import timezone +from freezegun import freeze_time + from django_ca.models import ( AcmeAccount, AcmeAuthorization, @@ -27,6 +30,7 @@ CertificateAuthority, ) from django_ca.tests.admin.assertions import assert_changelist_response +from django_ca.tests.base.constants import TIMESTAMPS from django_ca.tests.base.mixins import StandardAdminViewTestCaseMixin from django_ca.tests.base.typehints import DjangoCAModelTypeVar from django_ca.tests.base.utils import override_tmpcadir @@ -151,11 +155,17 @@ def test_expired_filter(self) -> None: ) assert_changelist_response(self.client.get(f"{self.changelist_url}?expired=1")) - with self.freeze_time("everything_expired"): - assert_changelist_response(self.client.get(f"{self.changelist_url}?expired=0")) - assert_changelist_response( - self.client.get(f"{self.changelist_url}?expired=1"), self.order1, self.order2 - ) + @override_tmpcadir() + @freeze_time(TIMESTAMPS["everything_expired"]) + def test_expired_filter_with_everything_expired(self) -> None: + """Test the "expired" filter when everything is expired.""" + self.client.force_login(self.user) + now = timezone.now() + AcmeOrder.objects.all().update(expires=now - timedelta(days=10)) + assert_changelist_response(self.client.get(f"{self.changelist_url}?expired=0")) + assert_changelist_response( + self.client.get(f"{self.changelist_url}?expired=1"), self.order1, self.order2 + ) class AcmeAuthorizationViewsTestCase(AcmeAdminTestCaseMixin[AcmeAuthorization], TestCase): diff --git a/ca/django_ca/tests/test_managers.py b/ca/django_ca/tests/test_managers.py index bbfe387be..92adf2c3b 100644 --- a/ca/django_ca/tests/test_managers.py +++ b/ca/django_ca/tests/test_managers.py @@ -497,10 +497,8 @@ def test_default_with_not_yet_valid(root: CertificateAuthority, settings: Settin @override_settings(CA_DEFAULT_CA="") -@pytest.mark.usefixtures("root") -@pytest.mark.usefixtures("child") -@pytest.mark.usefixtures("ed448") -@pytest.mark.usefixtures("ed25519") +@pytest.mark.freeze_time(TIMESTAMPS["everything_valid"]) +@pytest.mark.usefixtures("root", "child", "ed448", "ed25519") def test_default_with_no_default_ca() -> None: """Test what is returned when **no** CA is configured as default.""" ca = sorted(CertificateAuthority.objects.all(), key=lambda ca: (ca.expires, ca.serial))[-1] @@ -508,8 +506,7 @@ def test_default_with_no_default_ca() -> None: @pytest.mark.freeze_time(TIMESTAMPS["everything_expired"]) -@pytest.mark.usefixtures("root") -@pytest.mark.usefixtures("child") +@pytest.mark.usefixtures("root", "child") def test_default_with_expired_cas() -> None: """Test that exception is raised if no CA is currently valid.""" with assert_improperly_configured(r"^No CA is currently usable\.$"): diff --git a/ca/django_ca/tests/test_verification.py b/ca/django_ca/tests/test_verification.py index 46951e34d..3188895c5 100644 --- a/ca/django_ca/tests/test_verification.py +++ b/ca/django_ca/tests/test_verification.py @@ -14,6 +14,7 @@ """Validate certificates using the openssl command line tool.""" import os +import re import shlex import subprocess import tempfile @@ -25,367 +26,339 @@ from cryptography.hazmat.primitives.serialization import Encoding from cryptography.x509.oid import ExtensionOID, NameOID -from django.test import TestCase from django.urls import reverse -from freezegun import freeze_time +import pytest +from pytest_django.fixtures import SettingsWrapper from django_ca.key_backends import key_backends from django_ca.key_backends.storages import CreatePrivateKeyOptions from django_ca.models import CertificateAuthority, X509CertMixin -from django_ca.tests.base.constants import CERT_DATA, TIMESTAMPS -from django_ca.tests.base.mixins import TestCaseMixin -from django_ca.tests.base.utils import cmd, override_tmpcadir, uri - - -class CRLValidationTestCase(TestCaseMixin, TestCase): - """CRL validation tests.""" - - def assertFullName( # pylint: disable=invalid-name - self, - crl: x509.CertificateRevocationList, - expected: Optional[list[x509.GeneralName]] = None, - ) -> None: - """Assert that the full name of the CRL matches `expected`.""" - idp = crl.extensions.get_extension_for_class(x509.IssuingDistributionPoint).value - self.assertEqual(idp.full_name, expected) - - def assertNoIssuingDistributionPoint( # pylint: disable=invalid-name - self, crl: x509.CertificateRevocationList - ) -> None: - """Assert that the given CRL has *no* IssuingDistributionPoint extension.""" - try: - idp = crl.extensions.get_extension_for_class(x509.IssuingDistributionPoint) - self.fail(f"CRL contains an IssuingDistributionPoint extension: {idp}") - except x509.ExtensionNotFound: - pass - - def assertScope( # pylint: disable=invalid-name - self, - crl: x509.CertificateRevocationList, - ca: bool = False, - user: bool = False, - attribute: bool = False, - ) -> None: - """Assert that the scope at `path` is `expected`.""" - idp = crl.extensions.get_extension_for_class(x509.IssuingDistributionPoint).value - self.assertIs(idp.only_contains_ca_certs, ca, idp) - self.assertIs(idp.only_contains_user_certs, user) - self.assertIs(idp.only_contains_attribute_certs, attribute) - - def init_ca(self, name: str, **kwargs: Any) -> CertificateAuthority: - """Create a CA.""" - subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)]) - key_backend = key_backends["default"] - key_backend_options = CreatePrivateKeyOptions(key_type="RSA", password=None, path="ca", key_size=1024) - if kwargs.get("parent"): - kwargs["use_parent_private_key_options"] = key_backend.use_model(password=None) - return CertificateAuthority.objects.init(name, key_backend, key_backend_options, subject, **kwargs) - - @contextmanager - def crl( - self, ca: CertificateAuthority, **kwargs: Any - ) -> Iterator[tuple[str, x509.CertificateRevocationList]]: - """Dump CRL to a tmpdir, yield path to it.""" - kwargs["ca"] = ca - with tempfile.TemporaryDirectory() as tempdir: - path = os.path.join(tempdir, f"{ca.name}.{kwargs.get('scope')}.crl") - cmd("dump_crl", path, **kwargs) - - with open(path, "rb") as stream: - crl = x509.load_pem_x509_crl(stream.read()) - - yield path, crl - - @contextmanager - def dumped(self, *certificates: X509CertMixin) -> Iterator[list[str]]: - """Dump certificates to a tempdir, yield list of paths.""" - with tempfile.TemporaryDirectory() as tempdir: - paths = [] - for cert in certificates: - path = os.path.join(tempdir, f"{cert.serial}.pem") - paths.append(path) - with open(path, "w", encoding="ascii") as stream: - stream.write(cert.pub.pem) - - yield paths - - @contextmanager - def sign_cert( - self, ca: CertificateAuthority, hostname: str = "example.com", **kwargs: Any - ) -> Iterator[str]: - """Create a signed certificate in a temporary directory.""" - stdin = CERT_DATA["root-cert"]["csr"]["parsed"].public_bytes(Encoding.PEM) - # subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, hostname)]) - subject = f"CN={hostname}" - - with tempfile.TemporaryDirectory() as tempdir: - out_path = os.path.join(tempdir, f"{hostname}.pem") - with freeze_time(TIMESTAMPS["everything_valid"]): - cmd( - "sign_cert", - ca=ca, - subject_format="rfc4514", - subject=subject, - out=out_path, - stdin=stdin, - **kwargs, - ) - yield out_path - - def openssl(self, command: str, *args: str, code: int = 0, **kwargs: str) -> None: - """Run openssl.""" - exp_stdout = kwargs.pop("stdout", False) - exp_stderr = kwargs.pop("stderr", False) - command = command.format(*args, **kwargs) - if kwargs.pop("verbose", False): - print(f"openssl {command}") - proc = subprocess.run(["openssl", *shlex.split(command)], capture_output=True, check=False) - stdout = proc.stdout.decode("utf-8") - stderr = proc.stderr.decode("utf-8") - self.assertEqual(proc.returncode, code, stderr) - if isinstance(exp_stdout, str): - self.assertRegex(stdout, exp_stdout) - if isinstance(exp_stderr, str): - self.assertRegex(stderr, exp_stderr) - - def verify( - self, - command: str, - *args: str, - untrusted: Optional[Iterable[str]] = None, - crl: Optional[Iterable[str]] = None, - code: int = 0, - **kwargs: str, - ) -> None: - """Run openssl verify.""" - if untrusted: - untrusted_args = " ".join(f"-untrusted {path}" for path in untrusted) - command = f"{untrusted_args} {command}" - if crl: - crlfile_args = " ".join(f"-CRLfile {path}" for path in crl) - command = f"{crlfile_args} {command}" - - self.openssl(f"verify {command}", *args, code=code, **kwargs) - - @override_tmpcadir() - def test_root_ca(self) -> None: - """Try validating a root CA.""" - name = "Root" - ca = self.init_ca(name) - - # Very simple validation of the Root CRL - with self.dumped(ca) as paths: - self.verify("-CAfile {0} {0}", *paths) +from django_ca.tests.base.constants import CERT_DATA +from django_ca.tests.base.utils import ( + cmd, + crl_distribution_points, + distribution_point, + override_tmpcadir, + uri, +) + +pytestmark = [pytest.mark.usefixtures("tmpcadir"), pytest.mark.django_db] + + +def assert_full_name( + parsed_crl: x509.CertificateRevocationList, expected: Optional[list[x509.GeneralName]] = None +) -> None: + """Assert that the full name of the Issuing Distribution Point of the CRL matches `expected`.""" + idp = parsed_crl.extensions.get_extension_for_class(x509.IssuingDistributionPoint).value + assert idp.full_name == expected + + +def assert_no_issuing_distribution_point(parsed_crl: x509.CertificateRevocationList) -> None: + """Assert that the given CRL has *no* IssuingDistributionPoint extension.""" + try: + idp = parsed_crl.extensions.get_extension_for_class(x509.IssuingDistributionPoint) + pytest.fail(f"CRL contains an IssuingDistributionPoint extension: {idp}") + except x509.ExtensionNotFound: + pass + + +def assert_scope( + parsed_crl: x509.CertificateRevocationList, ca: bool = False, user: bool = False, attribute: bool = False +) -> None: + """Assert that the scope in the Issuing Distribution Point matches what we expect.""" + idp = parsed_crl.extensions.get_extension_for_class(x509.IssuingDistributionPoint).value + assert idp.only_contains_ca_certs is ca, idp + assert idp.only_contains_user_certs is user, idp + assert idp.only_contains_attribute_certs is attribute, idp + + +def init_ca(name: str, **kwargs: Any) -> CertificateAuthority: + """Create a CA.""" + subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)]) + key_backend = key_backends["default"] + key_backend_options = CreatePrivateKeyOptions(key_type="RSA", password=None, path="ca", key_size=1024) + if kwargs.get("parent"): + kwargs["use_parent_private_key_options"] = key_backend.use_model(password=None) + return CertificateAuthority.objects.init(name, key_backend, key_backend_options, subject, **kwargs) + + +@contextmanager +def crl(ca: CertificateAuthority, **kwargs: Any) -> Iterator[tuple[str, x509.CertificateRevocationList]]: + """Dump CRL to a tmpdir, yield path to it.""" + kwargs["ca"] = ca + with tempfile.TemporaryDirectory() as tempdir: + path = os.path.join(tempdir, f"{ca.name}.{kwargs.get('scope')}.crl") + cmd("dump_crl", path, **kwargs) + + with open(path, "rb") as stream: + loaded_crl = x509.load_pem_x509_crl(stream.read()) + + yield path, loaded_crl + + +@contextmanager +def dumped(*certificates: X509CertMixin) -> Iterator[list[str]]: + """Dump certificates to a tempdir, yield list of paths.""" + with tempfile.TemporaryDirectory() as tempdir: + paths = [] + for cert in certificates: + path = os.path.join(tempdir, f"{cert.serial}.pem") + paths.append(path) + with open(path, "w", encoding="ascii") as stream: + stream.write(cert.pub.pem) + + yield paths + + +@contextmanager +def sign_cert(ca: CertificateAuthority, hostname: str = "example.com", **kwargs: Any) -> Iterator[str]: + """Create a signed certificate in a temporary directory.""" + stdin = CERT_DATA["root-cert"]["csr"]["parsed"].public_bytes(Encoding.PEM) + subject = f"CN={hostname}" + + with tempfile.TemporaryDirectory() as tempdir: + out_path = os.path.join(tempdir, f"{hostname}.pem") + cmd( + "sign_cert", + ca=ca, + subject_format="rfc4514", + subject=subject, + out=out_path, + stdin=stdin, + **kwargs, + ) + yield out_path + + +def openssl(command: str, *args: str, code: int = 0, **kwargs: str) -> None: + """Run openssl.""" + exp_stdout = kwargs.pop("stdout", False) + exp_stderr = kwargs.pop("stderr", False) + command = command.format(*args, **kwargs) + if kwargs.pop("verbose", False): + print(f"openssl {command}") + proc = subprocess.run(["openssl", *shlex.split(command)], capture_output=True, check=False) + stdout = proc.stdout.decode("utf-8") + stderr = proc.stderr.decode("utf-8") + assert proc.returncode == code, stderr + if isinstance(exp_stdout, str): + assert re.search(exp_stdout, stdout) is not None, stdout + if isinstance(exp_stderr, str): + assert re.search(exp_stderr, stderr) is not None, stderr + + +def verify( + command: str, + *args: str, + untrusted: Optional[Iterable[str]] = None, + crl_path: Optional[Iterable[str]] = None, + code: int = 0, + **kwargs: str, +) -> None: + """Run openssl verify.""" + if untrusted: + untrusted_args = " ".join(f"-untrusted {path}" for path in untrusted) + command = f"{untrusted_args} {command}" + if crl_path: + crlfile_args = " ".join(f"-CRLfile {path}" for path in crl_path) + command = f"{crlfile_args} {command}" + + openssl(f"verify {command}", *args, code=code, **kwargs) + + +def test_root_ca(ca_name: str) -> None: + """Try validating a root CA.""" + ca = init_ca(ca_name) + + # Very simple validation of the Root CRL + with dumped(ca) as paths: + verify("-CAfile {0} {0}", *paths) + + # Create a CRL too and include it + with dumped(ca) as paths, crl(ca, scope="ca") as (crl_path, crl_parsed): + verify("-CAfile {0} -crl_check_all {0}", *paths, crl_path=[crl_path]) + + # Try again with no scope + with dumped(ca) as paths, crl(ca) as (crl_path, crl_parsed): + verify("-CAfile {0} -crl_check_all {0}", *paths, crl_path=[crl_path]) + + # Try with cert scope (fails because of wrong scope + with ( + dumped(ca) as paths, + crl(ca, scope="user") as (crl_path, crl_parsed), + pytest.raises(AssertionError), + ): + verify("-CAfile {0} -crl_check_all {0}", *paths, crl_path=[crl_path]) + + +def test_root_ca_cert(ca_name: str) -> None: + """Try validating a cert issued by the root CA.""" + ca = init_ca(ca_name) + + with dumped(ca) as paths, sign_cert(ca) as cert: + verify("-CAfile {0} {cert}", *paths, cert=cert) # Create a CRL too and include it - with self.dumped(ca) as paths, self.crl(ca, scope="ca") as (crl_path, crl): - self.verify("-CAfile {0} -crl_check_all {0}", *paths, crl=[crl_path]) - - # Try again with no scope - with self.dumped(ca) as paths, self.crl(ca) as (crl_path, crl): - self.verify("-CAfile {0} -crl_check_all {0}", *paths, crl=[crl_path]) - - # Try with cert scope (fails because of wrong scope - with ( - self.dumped(ca) as paths, - self.crl(ca, scope="user") as (crl_path, crl), - self.assertRaises(AssertionError), - ): - self.verify("-CAfile {0} -crl_check_all {0}", *paths, crl=[crl_path]) - - @override_tmpcadir(CA_DEFAULT_HOSTNAME="") - def test_root_ca_cert(self) -> None: - """Try validating a cert issued by the root CA.""" - name = "Root" - ca = self.init_ca(name) - - with self.dumped(ca) as paths, self.sign_cert(ca) as cert: - self.verify("-CAfile {0} {cert}", *paths, cert=cert) - - # Create a CRL too and include it - with self.crl(ca, scope="user") as (crl_path, crl): - self.assertScope(crl, user=True) - self.verify("-CAfile {0} -crl_check {cert}", *paths, crl=[crl_path], cert=cert) - - # for crl_check_all, we also need the root CRL - with self.crl(ca, scope="ca") as (crl2_path, crl2): - self.assertScope(crl2, ca=True) - self.verify( - "-CAfile {0} -crl_check_all {cert}", *paths, crl=[crl_path, crl2_path], cert=cert - ) - - # Try a single CRL with a global scope - with self.crl(ca, scope=None) as (crl_global_path, crl_global): - self.assertNoIssuingDistributionPoint(crl_global) - self.verify("-CAfile {0} -crl_check_all {cert}", *paths, crl=[crl_global_path], cert=cert) - - @override_tmpcadir(CA_DEFAULT_HOSTNAME="example.com") - def test_ca_default_hostname(self) -> None: - """Test that CA_DEFAULT_HOSTNAME does not lead to problems.""" - ca = self.init_ca("root") - # Root CAs have no CRLDistributionPoints - self.assertNotIn(ExtensionOID.CRL_DISTRIBUTION_POINTS, ca.extensions) - - with self.dumped(ca) as paths, self.sign_cert(ca) as cert: - with self.crl(ca) as (crl_path, crl): # test global CRL - self.assertNoIssuingDistributionPoint(crl) - self.verify("-trusted {0} -crl_check {cert}", *paths, crl=[crl_path], cert=cert) - self.verify("-trusted {0} -crl_check_all {cert}", *paths, crl=[crl_path], cert=cert) - - with self.crl(ca, scope="user") as (crl_path, crl): # test user-only CRL - self.assertScope(crl, user=True) - self.verify("-trusted {0} -crl_check {cert}", *paths, crl=[crl_path], cert=cert) - # crl_check_all does not work, b/c the scope is only "user" - self.verify( - "-trusted {0} -crl_check_all {cert}", - *paths, - crl=[crl_path], - cert=cert, - code=2, - stderr="[dD]ifferent CRL scope", + with crl(ca, scope="user") as (crl_path, crl_parsed): + assert_scope(crl_parsed, user=True) + verify("-CAfile {0} -crl_check {cert}", *paths, crl_path=[crl_path], cert=cert) + + # for crl_check_all, we also need the root CRL + with crl(ca, scope="ca") as (crl2_path, crl2): + assert_scope(crl2, ca=True) + verify("-CAfile {0} -crl_check_all {cert}", *paths, crl_path=[crl_path, crl2_path], cert=cert) + + # Try a single CRL with a global scope + with crl(ca, scope=None) as (crl_global_path, crl_global): + assert_no_issuing_distribution_point(crl_global) + verify("-CAfile {0} -crl_check_all {cert}", *paths, crl_path=[crl_global_path], cert=cert) + + +def test_ca_default_hostname() -> None: + """Test that CA_DEFAULT_HOSTNAME does not lead to problems.""" + ca = init_ca("root") + # Root CAs have no CRLDistributionPoints + assert ExtensionOID.CRL_DISTRIBUTION_POINTS not in ca.extensions + + with dumped(ca) as paths, sign_cert(ca) as cert: + with crl(ca) as (crl_path, crl_parsed): # test global CRL + assert_no_issuing_distribution_point(crl_parsed) + verify("-trusted {0} -crl_check {cert}", *paths, crl_path=[crl_path], cert=cert) + verify("-trusted {0} -crl_check_all {cert}", *paths, crl_path=[crl_path], cert=cert) + + with crl(ca, scope="user") as (crl_path, crl_parsed): # test user-only CRL + assert_scope(crl_parsed, user=True) + verify("-trusted {0} -crl_check {cert}", *paths, crl_path=[crl_path], cert=cert) + # crl_check_all does not work, b/c the scope is only "user" + verify( + "-trusted {0} -crl_check_all {cert}", + *paths, + crl_path=[crl_path], + cert=cert, + code=2, + stderr="[dD]ifferent CRL scope", + ) + + +@override_tmpcadir(CA_DEFAULT_HOSTNAME="") +def test_intermediate_ca(ca_name: str) -> None: + """Validate intermediate CA and its certs.""" + root = init_ca(f"{ca_name}_root", path_length=2) + child = init_ca(f"{ca_name}_child", parent=root, path_length=1) + grandchild = init_ca(f"{ca_name}_grandchild", parent=child) + + # Verify the state of the CAs themselves. + assert ExtensionOID.CRL_DISTRIBUTION_POINTS not in root.extensions + assert ExtensionOID.CRL_DISTRIBUTION_POINTS not in child.extensions + assert ExtensionOID.CRL_DISTRIBUTION_POINTS not in grandchild.extensions + + with dumped(root, child, grandchild) as paths: + untrusted = paths[1:] + # Simple validation of the CAs + verify("-CAfile {0} {1}", *paths) + verify("-CAfile {0} -untrusted {1} {2}", *paths) + + # Try validation with CRLs + with crl(root, scope="ca") as (crl1_path, crl1), crl(child, scope="ca") as (crl2_path, crl2): + verify("-CAfile {0} -untrusted {1} -crl_check_all {2}", *paths, crl_path=[crl1_path, crl2_path]) + + with sign_cert(child) as cert, crl(child, scope="user") as (crl3_path, crl3): + verify("-CAfile {0} -untrusted {1} {cert}", *paths, cert=cert) + verify( + "-CAfile {0} -untrusted {1} {cert}", *paths, cert=cert, crl_path=[crl1_path, crl3_path] ) - @override_tmpcadir(CA_DEFAULT_HOSTNAME="") - def test_intermediate_ca(self) -> None: - """Validate intermediate CA and its certs.""" - root = self.init_ca("Root", path_length=2) - child = self.init_ca("Child", parent=root, path_length=1) - grandchild = self.init_ca("Grandchild", parent=child) - - # Verify the state of the CAs themselves. - self.assertNotIn(ExtensionOID.CRL_DISTRIBUTION_POINTS, root.extensions) - self.assertNotIn(ExtensionOID.CRL_DISTRIBUTION_POINTS, child.extensions) - self.assertNotIn(ExtensionOID.CRL_DISTRIBUTION_POINTS, grandchild.extensions) - - with self.dumped(root, child, grandchild) as paths: - untrusted = paths[1:] - # Simple validation of the CAs - self.verify("-CAfile {0} {1}", *paths) - self.verify("-CAfile {0} -untrusted {1} {2}", *paths) - - # Try validation with CRLs with ( - self.crl(root, scope="ca") as (crl1_path, crl1), - self.crl(child, scope="ca") as ( - crl2_path, - crl2, - ), + sign_cert(grandchild) as cert, + crl(child, scope="ca") as (crl4_path, crl4), + crl(grandchild, scope="user") as (crl6_path, crl6), ): - self.verify( - "-CAfile {0} -untrusted {1} -crl_check_all {2}", *paths, crl=[crl1_path, crl2_path] - ) - - with self.sign_cert(child) as cert, self.crl(child, scope="user") as (crl3_path, crl3): - self.verify("-CAfile {0} -untrusted {1} {cert}", *paths, cert=cert) - self.verify( - "-CAfile {0} -untrusted {1} {cert}", *paths, cert=cert, crl=[crl1_path, crl3_path] - ) - - with ( - self.sign_cert(grandchild) as cert, - self.crl(child, scope="ca") as ( - crl4_path, - crl4, - ), - self.crl(grandchild, scope="user") as (crl6_path, crl6), - ): - self.verify("-CAfile {0} {cert}", *paths, untrusted=untrusted, cert=cert) - self.verify( - "-CAfile {0} -crl_check_all {cert}", - *paths, - untrusted=untrusted, - crl=[crl1_path, crl4_path, crl6_path], - cert=cert, - ) - - @override_tmpcadir(CA_DEFAULT_HOSTNAME="example.com") - def test_intermediate_ca_default_hostname(self) -> None: - """Test that a changing CA_DEFAULT_HOSTNAME does not lead to problems.""" - root = self.init_ca("Root", path_length=2) - child = self.init_ca("Child", parent=root, path_length=1) - grandchild = self.init_ca("Grandchild", parent=child) - - child_ca_crl = reverse("django_ca:ca-crl", kwargs={"serial": root.serial}) - grandchild_ca_crl = reverse("django_ca:ca-crl", kwargs={"serial": child.serial}) - - # Verify the state of the CAs themselves. - self.assertNotIn(ExtensionOID.CRL_DISTRIBUTION_POINTS, root.extensions) - self.assertEqual( - child.extensions[ExtensionOID.CRL_DISTRIBUTION_POINTS], - self.crl_distribution_points([uri(f"http://example.com{child_ca_crl}")]), - ) - self.assertEqual( - grandchild.extensions[ExtensionOID.CRL_DISTRIBUTION_POINTS], - self.crl_distribution_points([uri(f"http://example.com{grandchild_ca_crl}")]), - ) - - with self.dumped(root, child, grandchild) as paths, self.crl(root, scope="ca") as (crl_path, crl): - # Simple validation of the CAs - self.verify("-trusted {0} {1}", *paths) - self.verify("-trusted {0} -untrusted {1} {2}", *paths) - - with self.crl(child, scope="ca") as (crl2_path, crl2): - self.assertFullName(crl, None) - self.assertFullName(crl2, [uri(f"http://example.com{grandchild_ca_crl}")]) - self.verify( - "-trusted {0} -untrusted {1} -crl_check_all {2}", *paths, crl=[crl_path, crl2_path] - ) - - # Globally scoped CRLs do not validate, as the CRL will contain a different full name from the - # CRLdp extension - with self.crl(child) as (crl2_path, crl2): - self.assertFullName(crl, None) - # self.assertFullName(crl2, [uri(f"http://example.com{grandchild_ca_crl}")]) - self.verify( - "-trusted {0} -untrusted {1} -crl_check_all {2}", + verify("-CAfile {0} {cert}", *paths, untrusted=untrusted, cert=cert) + verify( + "-CAfile {0} -crl_check_all {cert}", *paths, - crl=[crl_path, crl2_path], - code=2, - stderr="[dD]ifferent CRL scope", + untrusted=untrusted, + crl_path=[crl1_path, crl4_path, crl6_path], + cert=cert, ) - # Changing the default hostname setting should not change the validation result - with ( - self.settings(CA_DEFAULT_HOSTNAME="example.net"), - self.crl(root, scope="ca") as ( - crl_path, - crl, - ), - self.crl(child, scope="ca") as ( - crl2_path, - crl2, - ), - ): - # Known but not easily fixable issue: If CA_DEFAULT_HOSTNAME is changed, CRLs will get wrong - # full name and validation fails. - self.assertFullName(crl, None) - # self.assertFullName(crl2, [uri(f"http://example.com{grandchild_ca_crl}")]) - self.verify( - "-trusted {0} -untrusted {1} -crl_check_all {2}", - *paths, - crl=[crl_path, crl2_path], - code=2, - stderr="[dD]ifferent CRL scope", - ) - # Again, global CRLs do not validate - with ( - self.settings(CA_DEFAULT_HOSTNAME="example.net"), - self.crl(root, scope="ca") as ( - crl_path, - crl, - ), - self.crl(child) as ( - crl2_path, - crl2, - ), - ): - self.assertFullName(crl, None) - self.verify( - "-trusted {0} -untrusted {1} -crl_check_all {2}", - *paths, - crl=[crl_path, crl2_path], - code=2, - stderr="[dD]ifferent CRL scope", - ) +@override_tmpcadir(CA_DEFAULT_HOSTNAME="example.com") +def test_intermediate_ca_default_hostname(ca_name: str, settings: SettingsWrapper) -> None: + """Test that a changing CA_DEFAULT_HOSTNAME does not lead to problems.""" + root = init_ca(f"{ca_name}_root", path_length=2) + child = init_ca(f"{ca_name}_child", parent=root, path_length=1) + grandchild = init_ca(f"{ca_name}_grandchild", parent=child) + + child_ca_crl = reverse("django_ca:ca-crl", kwargs={"serial": root.serial}) + grandchild_ca_crl = reverse("django_ca:ca-crl", kwargs={"serial": child.serial}) + + # Verify the state of the CAs themselves. + assert ExtensionOID.CRL_DISTRIBUTION_POINTS not in root.extensions + assert child.extensions[ExtensionOID.CRL_DISTRIBUTION_POINTS] == crl_distribution_points( + distribution_point([uri(f"http://example.com{child_ca_crl}")]) + ) + + assert grandchild.extensions[ExtensionOID.CRL_DISTRIBUTION_POINTS] == crl_distribution_points( + distribution_point([uri(f"http://example.com{grandchild_ca_crl}")]), + ) + + with ( + dumped(root, child, grandchild) as paths, + crl(root, scope="ca") as (root_ca_crl_path, root_ca_crl_parsed), + ): + # Simple validation of the CAs + verify("-trusted {0} {1}", *paths) + verify("-trusted {0} -untrusted {1} {2}", *paths) + + with crl(child, scope="ca") as (child_ca_crl_path, child_ca_crl_parsed): + assert_full_name(child_ca_crl_parsed, [uri(f"http://example.com{grandchild_ca_crl}")]) + verify( + "-trusted {0} -untrusted {1} -crl_check_all {2}", + *paths, + crl_path=[root_ca_crl_path, child_ca_crl_path], + ) + + # Globally scoped CRLs do not validate, as the CRL will contain a different full name from the + # CRLdp extension + with crl(child) as (child_crl_path, child_crl_parsed): + # assert_full_name(crl, None) + # assert_full_name(crl2, [uri(f"http://example.com{grandchild_ca_crl}")]) + verify( + "-trusted {0} -untrusted {1} -crl_check_all {2}", + *paths, + crl_path=[root_ca_crl_path, child_crl_path], + code=2, + stderr="[dD]ifferent CRL scope", + ) + + # Changing the default hostname setting should not change the validation result + settings.CA_DEFAULT_HOSTNAME = "example.net" + with crl(root, scope="ca") as (crl_path, crl_parsed), crl(child, scope="ca") as (crl2_path, crl2): + # Known but not easily fixable issue: If CA_DEFAULT_HOSTNAME is changed, CRLs will get wrong + # full name and validation fails. + assert_full_name(crl_parsed, None) + # assert_full_name(crl2, [uri(f"http://example.com{grandchild_ca_crl}")]) + verify( + "-trusted {0} -untrusted {1} -crl_check_all {2}", + *paths, + crl_path=[crl_path, crl2_path], + code=2, + stderr="[dD]ifferent CRL scope", + ) + + # Again, global CRLs do not validate + settings.CA_DEFAULT_HOSTNAME = "example.net" + with ( + crl(root, scope="ca") as (crl_path, crl_parsed), + crl(child) as (crl2_path, crl_parsed_2), + ): + assert_full_name(crl_parsed, None) + verify( + "-trusted {0} -untrusted {1} -crl_check_all {2}", + *paths, + crl_path=[crl_path, crl2_path], + code=2, + stderr="[dD]ifferent CRL scope", + ) diff --git a/docs/source/dev/testing.rst b/docs/source/dev/testing.rst index 93c93b6e4..9a15c52fc 100644 --- a/docs/source/dev/testing.rst +++ b/docs/source/dev/testing.rst @@ -60,23 +60,45 @@ Generated fixtures {name}_pub - :py:class:`~cryptography.x509.Certificate` Certificate loaded from test fixture data. - Available for every CA generated in the test fixtures and every certificate (including unusable - certificates). Examples: ``root_pub``, ``root_cert``, ``profile_server_pub`` and - ``globalsign_dv-cert_pub``. + Available for every CA generated in the test fixtures and every certificate. Examples: ``root_pub``, + ``root_cert_pub``, ``profile_server_pub``. Contributed certificates are prefixed with ``contrib_`` + (see below). {ca_name} - :py:class:`~django_ca.models.CertificateAuthority` Certificate authority model **without** usable private key files. Available for every CA generated in the test fixtures. Using this fixture enables database access. +{cert} - :py:class:`~django_ca.models.Certificate` + Certificate model for certificates generated in test fixture data. + +contrib_{ca_name} - :py:class:`~django_ca.models.CertificateAuthority` + Certificate authority model for a contributed certificate. + + Examples: ``contrib_godaddy_g2_root``, ``contrib_geotrust`` and ``contrib_startssl_class3``. + +contrib_{ca_name}_cert - :py:class:`~django_ca.models.Certificate` + Certificate model for contributed certificates loaded from test fixture data. + + Examples: ``contrib_godaddy_g2_root_cert``, ``contrib_geotrust_cert`` and + ``contrib_startssl_class3_cert``. + +contrib_{ca_name}_cert_pub - :py:class:`~cryptography.x509.Certificate` + Certificate for contributed certificates loaded from test fixture data. + + Examples: ``contrib_godaddy_g2_root_cert_pub``, ``contrib_geotrust_cert_pub`` and + ``contrib_startssl_class3_cert_pub``. + +contrib_{ca_name}_pub - :py:class:`~cryptography.x509.Certificate` + Certificate for contributed certificate authorities loaded from test fixture data. + + Examples: ``contrib_godaddy_g2_root_pub``, ``contrib_geotrust_pub`` and ``contrib_startssl_class3_pub``. + usable_{ca_name} - :py:class:`~django_ca.models.CertificateAuthority` Certificate authority model with usable private key files. Available for every CA generated in the test fixtures. -{cert} - :py:class:`~django_ca.models.Certificate` - Certificate model for certificates generated in test fixture data. - Mocks ===== diff --git a/tox.ini b/tox.ini index 179d61c9e..15dde5e64 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = pylint,docs,lint,mypy,demo,dist-test py{310,311,312}-dj{5.0}-cg{42}-acme{2.9}-pydantic{2.5,2.6} py{39,310,311,312}-dj{4.2}-cg{42}-acme{2.9}-pydantic{2.5,2.6} + faketime [testenv] skipsdist = True @@ -19,6 +20,17 @@ setenv = commands = pytest -v --basetemp="{env_tmp_dir}" --cov-report html:{envdir}/htmlcov/ --durations=20 {posargs} +[testenv:faketime] +skipsdist = True +deps = + -r requirements.txt + -r requirements/requirements-test.txt +setenv = + COVERAGE_FILE = {envdir}/.coverage +allowlist_externals = faketime +commands = + faketime -f "+100y" pytest -v --basetemp="{env_tmp_dir}" --cov-report html:{envdir}/htmlcov/ --durations=20 {posargs} + [testenv:demo] basepython = python3 skipsdist = True