From 01f711030d8a3c969e33e2ca70d37c90c36c3b19 Mon Sep 17 00:00:00 2001 From: Mathias Ertl Date: Fri, 5 Jan 2024 20:42:40 +0100 Subject: [PATCH] remove the cn_in_san options --- ca/django_ca/admin.py | 38 +---- ca/django_ca/ca_settings.py | 3 - ca/django_ca/constants.py | 2 +- ca/django_ca/extensions/__init__.py | 1 + ca/django_ca/fields.py | 52 ++----- ca/django_ca/forms.py | 20 +-- .../management/commands/resign_cert.py | 1 - ca/django_ca/management/commands/sign_cert.py | 34 ---- ca/django_ca/managers.py | 6 - ca/django_ca/models.py | 41 +---- ca/django_ca/profiles.py | 54 +------ .../django_ca/admin/js/profilewidget.js | 4 - ca/django_ca/tasks.py | 1 - ca/django_ca/tests/admin/test_actions.py | 20 ++- ca/django_ca/tests/admin/test_add_cert.py | 123 ++------------- ca/django_ca/tests/base/mixins.py | 28 ---- ca/django_ca/tests/commands/test_sign_cert.py | 105 ++----------- ca/django_ca/tests/test_managers.py | 6 +- ca/django_ca/tests/test_models.py | 145 +----------------- ca/django_ca/tests/test_profiles.py | 117 +------------- ca/django_ca/typehints.py | 1 - ca/django_ca/widgets.py | 55 ++----- docs/source/changelog.rst | 2 + docs/source/profiles.rst | 1 - 24 files changed, 99 insertions(+), 761 deletions(-) diff --git a/ca/django_ca/admin.py b/ca/django_ca/admin.py index 19c901b45..6a74ca949 100644 --- a/ca/django_ca/admin.py +++ b/ca/django_ca/admin.py @@ -56,7 +56,7 @@ from django_object_actions import DjangoObjectActions from django_ca import ca_settings, constants -from django_ca.constants import EXTENSION_DEFAULT_CRITICAL, EXTENSION_KEY_OIDS, EXTENSION_KEYS, ReasonFlags +from django_ca.constants import EXTENSION_KEY_OIDS, EXTENSION_KEYS, ReasonFlags from django_ca.extensions import CERTIFICATE_EXTENSIONS, get_extension_name from django_ca.extensions.utils import certificate_policies_is_simple, extension_as_admin_html from django_ca.forms import ( @@ -123,6 +123,8 @@ ] QuerySetTypeVar = typing.TypeVar("QuerySetTypeVar", bound=QuerySet) +EXTENSION_FIELDS = tuple((key for key in CERTIFICATE_EXTENSIONS if key != "subject_alternative_name")) + @admin.register(Watcher) class WatcherAdmin(WatcherAdminBase): @@ -626,16 +628,7 @@ class CertificateAdmin(DjangoObjectActions, CertificateMixin[Certificate], Certi ), }, ), - ( - _("X.509 Extensions"), - { - "fields": CERTIFICATE_EXTENSIONS, - "classes": ( - "collapse", - "x509-extensions", - ), - }, - ), + (_("X.509 Extensions"), {"fields": EXTENSION_FIELDS, "classes": ("collapse", "x509-extensions")}), ) # same as add_fieldsets but without the csr @@ -654,10 +647,7 @@ class CertificateAdmin(DjangoObjectActions, CertificateMixin[Certificate], Certi ], }, ), - ( - _("X.509 Extensions"), - {"fields": CERTIFICATE_EXTENSIONS}, - ), + (_("X.509 Extensions"), {"fields": EXTENSION_FIELDS}), ) x509_fieldset_index = 1 @@ -726,17 +716,6 @@ def get_changeform_initial_data(self, request: HttpRequest) -> Dict[str, Any]: # resign the cert, so we add initial data from the original cert resign_obj = request._resign_obj # pylint: disable=protected-access - san = resign_obj.x509_extensions.get(ExtensionOID.SUBJECT_ALTERNATIVE_NAME) - if san is None: - san_value = [] - san_critical = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.SUBJECT_ALTERNATIVE_NAME] - else: - san_value = list(san.value) - san_critical = san.critical - - # Since Django 4.1, tuples are no longer passed to a MultiWidgets decompress() method. We must - # thus pass a three-tuple as initial value, each corresponding to the value of one of the widgets. - subject_alternative_name = (san_value, False, san_critical) if resign_obj.algorithm is not None: hash_algorithm_name = constants.HASH_ALGORITHM_NAMES[type(resign_obj.algorithm)] @@ -750,7 +729,6 @@ def get_changeform_initial_data(self, request: HttpRequest) -> Dict[str, Any]: "ca": resign_obj.ca, "profile": profile, "subject": resign_obj.subject, - "subject_alternative_name": subject_alternative_name, "watchers": resign_obj.watchers.all(), } @@ -1035,9 +1013,6 @@ def save_model( # type: ignore[override] # Set Subject Alternative Name from form extensions: Dict[x509.ObjectIdentifier, x509.Extension[x509.ExtensionType]] = {} - subject_alternative_name, cn_in_san = data["subject_alternative_name"] - if subject_alternative_name: - extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME] = subject_alternative_name # Update extensions handled through the form for key in CERTIFICATE_EXTENSIONS: @@ -1067,8 +1042,6 @@ def save_model( # type: ignore[override] if EXTENSION_KEYS[oid] in CERTIFICATE_EXTENSIONS: # already handled in form continue - if oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME: # already handled above - continue # Add any extension from the profile currently not changeable in the web interface extensions[oid] = ext # pragma: no cover # all extensions should be handled above! @@ -1083,7 +1056,6 @@ def save_model( # type: ignore[override] algorithm=data["algorithm"], expires=expires, extensions=extensions.values(), - cn_in_san=cn_in_san, password=data["password"], ) ) diff --git a/ca/django_ca/ca_settings.py b/ca/django_ca/ca_settings.py index 3a47e8a0e..b5a02a5a3 100644 --- a/ca/django_ca/ca_settings.py +++ b/ca/django_ca/ca_settings.py @@ -184,7 +184,6 @@ def _get_hash_algorithm(setting: str, default: "HashAlgorithms") -> "AllowedHash "description": _( "A certificate for an enduser, allows client authentication, code and email signing." ), - "cn_in_san": False, "extensions": { "key_usage": { "critical": True, @@ -206,7 +205,6 @@ def _get_hash_algorithm(setting: str, default: "HashAlgorithms") -> "AllowedHash }, "ocsp": { "description": _("A certificate for an OCSP responder."), - "cn_in_san": False, # CAs frequently use human-readable name as CN "add_ocsp_url": False, "autogenerated": True, "subject": False, @@ -290,7 +288,6 @@ def _get_hash_algorithm(setting: str, default: "HashAlgorithms") -> "AllowedHash for profile_name, profile in CA_PROFILES.items(): profile.setdefault("subject", CA_DEFAULT_SUBJECT) - profile.setdefault("cn_in_san", True) if subject := profile.get("subject"): profile["subject"] = _normalize_x509_name(subject, f"subject in {profile_name} profile.") diff --git a/ca/django_ca/constants.py b/ca/django_ca/constants.py index b1feb08e7..7180c16d1 100644 --- a/ca/django_ca/constants.py +++ b/ca/django_ca/constants.py @@ -168,7 +168,7 @@ class ExtendedKeyUsageOID(_ExtendedKeyUsageOID): ExtensionOID.SIGNED_CERTIFICATE_TIMESTAMPS: _( # defined in RFC 6962 "may or may not be critical (recommended: non-critical)" ), - ExtensionOID.SUBJECT_ALTERNATIVE_NAME: _("SHOULD mark this extension as non-critical"), + ExtensionOID.SUBJECT_ALTERNATIVE_NAME: _("SHOULD be non-critical"), ExtensionOID.SUBJECT_INFORMATION_ACCESS: _("MUST be non-critical"), ExtensionOID.SUBJECT_KEY_IDENTIFIER: _("MUST be non-critical"), ExtensionOID.TLS_FEATURE: _("SHOULD NOT be critical"), # defined in RFC 7633 diff --git a/ca/django_ca/extensions/__init__.py b/ca/django_ca/extensions/__init__.py index 98337c171..cedf00fb7 100644 --- a/ca/django_ca/extensions/__init__.py +++ b/ca/django_ca/extensions/__init__.py @@ -36,6 +36,7 @@ "issuer_alternative_name", "key_usage", "ocsp_no_check", + "subject_alternative_name", "tls_feature", ] ) diff --git a/ca/django_ca/fields.py b/ca/django_ca/fields.py index 97afe7559..9ed9a5105 100644 --- a/ca/django_ca/fields.py +++ b/ca/django_ca/fields.py @@ -24,7 +24,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -from django_ca import ca_settings, constants, utils, widgets +from django_ca import constants, utils, widgets from django_ca.constants import ( EXTENDED_KEY_USAGE_HUMAN_READABLE_NAMES, EXTENDED_KEY_USAGE_NAMES, @@ -32,7 +32,7 @@ REVOCATION_REASONS, ) from django_ca.extensions import get_extension_name -from django_ca.typehints import CRLExtensionTypeTypeVar, ExtensionTypeTypeVar +from django_ca.typehints import AlternativeNameTypeVar, CRLExtensionTypeTypeVar, ExtensionTypeTypeVar from django_ca.utils import parse_general_name from django_ca.widgets import KeyValueWidget, SubjectWidget @@ -290,6 +290,18 @@ def get_value(self, *value: Any) -> Optional[ExtensionTypeTypeVar]: """ +class AlternativeNameField(ExtensionField[AlternativeNameTypeVar]): + """Form field for a :py:class:`~cg:cryptography.x509.IssuerAlternativeName` extension.""" + + extension_type: Type[AlternativeNameTypeVar] + fields = (GeneralNamesField(required=False),) + + def get_value(self, value: List[x509.GeneralName]) -> Optional[AlternativeNameTypeVar]: + if not value: + return None + return self.extension_type(general_names=value) + + class MultipleChoiceExtensionField(ExtensionField[ExtensionTypeTypeVar]): """Base class for extensions that are basically a multiple choice field (plus critical).""" @@ -447,18 +459,12 @@ class FreshestCRLField(DistributionPointField[x509.FreshestCRL]): widget = widgets.FreshestCRLWidget -class IssuerAlternativeNameField(ExtensionField[x509.IssuerAlternativeName]): +class IssuerAlternativeNameField(AlternativeNameField[x509.IssuerAlternativeName]): """Form field for a :py:class:`~cg:cryptography.x509.IssuerAlternativeName` extension.""" extension_type = x509.IssuerAlternativeName - fields = (GeneralNamesField(required=False),) widget = widgets.IssuerAlternativeNameWidget - def get_value(self, value: List[x509.GeneralName]) -> Optional[x509.IssuerAlternativeName]: - if not value: - return None - return x509.IssuerAlternativeName(general_names=value) - class KeyUsageField(MultipleChoiceExtensionField[x509.KeyUsage]): """Form field for a :py:class:`~cg:cryptography.x509.KeyUsage` extension.""" @@ -486,38 +492,12 @@ def get_value(self, value: bool) -> Optional[x509.OCSPNoCheck]: return None -class SubjectAlternativeNameField(ExtensionField[x509.SubjectAlternativeName]): +class SubjectAlternativeNameField(AlternativeNameField[x509.SubjectAlternativeName]): """Form field for a :py:class:`~cg:cryptography.x509.SubjectAlternativeName` extension.""" extension_type = x509.SubjectAlternativeName - fields = ( - GeneralNamesField(required=False), - forms.BooleanField(required=False), - ) widget = widgets.SubjectAlternativeNameWidget - def compress( # type: ignore[override] # this is a special case - self, data_list: List[Any] - ) -> Tuple[Optional[x509.Extension[ExtensionTypeTypeVar]], bool]: - default_cn_in_san = ca_settings.CA_PROFILES[ca_settings.CA_DEFAULT_PROFILE]["cn_in_san"] - if not data_list: # pragma: no cover - return None, default_cn_in_san - - *value, critical = data_list - ext_value, cn_in_san = self.get_value(*value) - if ext_value is None: - return None, cn_in_san - ext = x509.Extension(critical=critical, oid=self.extension_type.oid, value=ext_value) - # TYPE NOTE: mypy complains about the non-generic type being returned - return ext, cn_in_san # type: ignore[return-value] - - def get_value( # type: ignore[override] - self, names: List[x509.GeneralName], cn_in_san: bool - ) -> Tuple[Optional[x509.SubjectAlternativeName], bool]: - if not names: - return None, cn_in_san - return x509.SubjectAlternativeName(general_names=names), cn_in_san - class TLSFeatureField(MultipleChoiceExtensionField[x509.TLSFeature]): """Form field for a :py:class:`~cg:cryptography.x509.TLSFeature` extension.""" diff --git a/ca/django_ca/forms.py b/ca/django_ca/forms.py index 4a4131ce5..95a9202a5 100644 --- a/ca/django_ca/forms.py +++ b/ca/django_ca/forms.py @@ -30,7 +30,6 @@ from django_ca import ca_settings, constants, fields from django_ca.models import Certificate, CertificateAuthority, X509CertMixin from django_ca.querysets import CertificateAuthorityQuerySet -from django_ca.utils import parse_general_name from django_ca.widgets import ProfileWidget if typing.TYPE_CHECKING: @@ -119,7 +118,7 @@ class CreateCertificateBaseForm(CertificateModelForm): subject = fields.SubjectField(label=_("Subject"), required=False) subject_alternative_name = fields.SubjectAlternativeNameField( required=False, - help_text=_("""Coma-separated list of alternative names for the certificate."""), + help_text=_("""Alternative names for the certificate (one per line)."""), ) profile = forms.ChoiceField( required=False, @@ -214,7 +213,7 @@ def clean(self) -> Optional[Dict[str, Any]]: password = typing.cast(Optional[str], data.get("password")) subject = typing.cast(Optional[x509.Name], data.get("subject")) algorithm = typing.cast(Optional[hashes.HashAlgorithm], data.get("algorithm")) - subject_alternative_name, include_common_name = data.get("subject_alternative_name", (None, False)) + subject_alternative_name = data.get("subject_alternative_name", (None, False)) subject_alternative_name = typing.cast( Optional[x509.Extension[x509.SubjectAlternativeName]], subject_alternative_name @@ -248,21 +247,6 @@ def clean(self) -> Optional[Dict[str, Any]]: if subject is not None: common_names = subject.get_attributes_for_oid(NameOID.COMMON_NAME) - # If the user decided to include any Common Names in the Subject Alternative Name extension, we check - # that they can be parsed as general name. - if include_common_name: - for common_name in common_names: - try: - parse_general_name(common_name.value) # type: ignore[arg-type] - except ValueError: - self.add_error( - "subject_alternative_name", - _( - "The CommonName cannot be parsed as general name. Either change the " - "CommonName or do not include it." - ), - ) - # Make sure that we have at least a Common Name *or* a Subject Alternative Name extension. if subject is not None and not common_names and not subject_alternative_name: self.add_error( diff --git a/ca/django_ca/management/commands/resign_cert.py b/ca/django_ca/management/commands/resign_cert.py index 346a0d1e3..caf987edc 100644 --- a/ca/django_ca/management/commands/resign_cert.py +++ b/ca/django_ca/management/commands/resign_cert.py @@ -192,7 +192,6 @@ def handle( # pylint: disable=too-many-locals # noqa: PLR0912,PLR0913 algorithm=algorithm, extensions=extensions.values(), password=password, - cn_in_san=False, # we already copy the SAN/CN from the original cert ) except Exception as ex: raise CommandError(ex) from ex diff --git a/ca/django_ca/management/commands/sign_cert.py b/ca/django_ca/management/commands/sign_cert.py index 85ba68b53..dddd68d9e 100644 --- a/ca/django_ca/management/commands/sign_cert.py +++ b/ca/django_ca/management/commands/sign_cert.py @@ -47,40 +47,8 @@ class Command(BaseSignCertCommand): --profile. The --subject option allows you to name a CommonName (which is not usually in the defaults) and override any default values.""" - def add_cn_in_san(self, parser: CommandParser) -> None: - """Add argument group for the CommonName-in-SubjectAlternativeName options.""" - if ca_settings.CA_PROFILES[ca_settings.CA_DEFAULT_PROFILE]["cn_in_san"]: - cn_in_san_default = " (default)" - cn_not_in_san_default = "" - else: - cn_in_san_default = "" - cn_not_in_san_default = " (default)" - - group = parser.add_argument_group( - "CommonName in subjectAltName", - """Whether or not to automatically include the CommonName (given in --subject) in the - list of subjectAltNames (given by --alt).""", - ) - group = group.add_mutually_exclusive_group() - - group.add_argument( - "--cn-not-in-san", - default=None, - action="store_false", - dest="cn_in_san", - help=f"Do not add the CommonName as subjectAlternativeName{cn_not_in_san_default}.", - ) - group.add_argument( - "--cn-in-san", - default=None, - action="store_true", - dest="cn_in_san", - help=f"Add the CommonName as subjectAlternativeName{cn_in_san_default}.", - ) - def add_arguments(self, parser: CommandParser) -> None: general_group = self.add_base_args(parser) - self.add_cn_in_san(parser) general_group.add_argument( "--csr", @@ -106,7 +74,6 @@ def handle( # pylint: disable=too-many-locals # noqa: PLR0912,PLR0913,PLR0915 expires: Optional[timedelta], watch: List[str], password: Optional[bytes], - cn_in_san: bool, csr_path: str, bundle: bool, profile: Optional[str], @@ -228,7 +195,6 @@ def handle( # pylint: disable=too-many-locals # noqa: PLR0912,PLR0913,PLR0915 ca, csr, profile=profile_obj, - cn_in_san=cn_in_san, expires=expires, extensions=extensions.values(), password=password, diff --git a/ca/django_ca/managers.py b/ca/django_ca/managers.py index b8777bbd1..d36b2d2f9 100644 --- a/ca/django_ca/managers.py +++ b/ca/django_ca/managers.py @@ -576,7 +576,6 @@ def create_cert( # noqa: PLR0913 expires: Expires = None, algorithm: Optional[AllowedHashTypes] = None, extensions: Optional[Iterable[x509.Extension[x509.ExtensionType]]] = None, - cn_in_san: Optional[bool] = None, add_crl_url: Optional[bool] = None, add_ocsp_url: Optional[bool] = None, add_issuer_url: Optional[bool] = None, @@ -606,10 +605,6 @@ def create_cert( # noqa: PLR0913 Passed to :py:func:`Profiles.create_cert() `. extensions : list or of :py:class:`~cg:cryptography.x509.Extension` Passed to :py:func:`Profiles.create_cert() `. - cn_in_san : bool, optional - Passed to :py:func:`Profiles.create_cert() `. - cn_in_san : bool, optional - Passed to :py:func:`Profiles.create_cert() `. add_crl_url : bool, optional Passed to :py:func:`Profiles.create_cert() `. add_ocsp_url : bool, optional @@ -634,7 +629,6 @@ def create_cert( # noqa: PLR0913 expires=expires, algorithm=algorithm, extensions=extensions, - cn_in_san=cn_in_san, add_crl_url=add_crl_url, add_ocsp_url=add_ocsp_url, add_issuer_url=add_issuer_url, diff --git a/ca/django_ca/models.py b/ca/django_ca/models.py index 76157ad87..2954efb58 100644 --- a/ca/django_ca/models.py +++ b/ca/django_ca/models.py @@ -784,7 +784,6 @@ def sign( algorithm: Optional[AllowedHashTypes] = None, expires: Optional[datetime] = None, extensions: Optional[Iterable[x509.Extension[x509.ExtensionType]]] = None, - cn_in_san: bool = True, password: Optional[Union[str, bytes]] = None, ) -> x509.Certificate: """Create a signed certificate. @@ -793,8 +792,7 @@ def sign( Required extensions are added if not provided. Unless already included in `extensions`, this function will add the AuthorityKeyIdentifier, BasicConstraints and SubjectKeyIdentifier extensions with values - coming from the certificate authority. The common names in `subject` are added to - SubjectAlternativeName if `cn_in_san` is ``True``. + coming from the certificate authority. Parameters ---------- @@ -809,9 +807,6 @@ def sign( extensions : list of :py:class:`~cg:cryptography.x509.Extension`, optional List of extensions to add to the certificates. The function will add some extensions unless provided here, see above for details. - cn_in_san : bool, optional - Include common names from the subject in the SubjectAlternativeName extension. ``True`` by - default. password : str or bytes, optional Password for loading the private key of the CA, if any. """ @@ -854,39 +849,6 @@ def sign( if ExtensionOID.AUTHORITY_KEY_IDENTIFIER not in exts: exts[ExtensionOID.AUTHORITY_KEY_IDENTIFIER] = self.get_authority_key_identifier_extension() - # Add CommonNames to the SubjectAlternativeName extension if cn_in_san == True - common_names = subject.get_attributes_for_oid(NameOID.COMMON_NAME) - san = typing.cast( - Optional[x509.Extension[x509.SubjectAlternativeName]], - exts.get(ExtensionOID.SUBJECT_ALTERNATIVE_NAME), - ) - if cn_in_san is True: - for raw_common_name in common_names: - try: - # TYPEHINT NOTE: NameAttribute.value may be bytes but must be str for COMMON_NAME. - # This is guaranteed by the NameAttribute constructor. - raw_common_name_value = typing.cast(str, raw_common_name.value) - cn = parse_general_name(raw_common_name_value) - except ValueError: - continue - - if not san: - san = x509.Extension( - oid=ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - critical=False, - value=x509.SubjectAlternativeName([cn]), - ) - exts[ExtensionOID.SUBJECT_ALTERNATIVE_NAME] = san - - elif cn not in san.value: - san = x509.Extension( - oid=ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - critical=exts[ExtensionOID.SUBJECT_ALTERNATIVE_NAME].critical, - value=x509.SubjectAlternativeName([*san.value, cn]), - ) - exts[ExtensionOID.SUBJECT_ALTERNATIVE_NAME] = san - # else: CommonName already in SubjectAlternativeName - extensions = exts.values() pre_sign_cert.send( sender=self.__class__, @@ -1067,7 +1029,6 @@ def generate_ocsp_key( # pylint: disable=too-many-locals # noqa: PLR0912 password=password, expires=expires, add_ocsp_url=False, - cn_in_san=False, ) cert_path = ca_storage.generate_filename(f"ocsp/{safe_serial}.pem") diff --git a/ca/django_ca/profiles.py b/ca/django_ca/profiles.py index 341aee75f..a03ba5cd1 100644 --- a/ca/django_ca/profiles.py +++ b/ca/django_ca/profiles.py @@ -41,7 +41,7 @@ SerializedProfile, SerializedPydanticName, ) -from django_ca.utils import get_cert_builder, merge_x509_names, parse_expires, parse_general_name, x509_name +from django_ca.utils import get_cert_builder, merge_x509_names, parse_expires, x509_name if typing.TYPE_CHECKING: from django_ca.models import CertificateAuthority @@ -115,7 +115,12 @@ def __init__( # noqa: PLR0913 except KeyError as ex: raise ValueError(f"{algorithm}: Unknown hash algorithm.") from ex - self.cn_in_san = cn_in_san + if cn_in_san is not None: # pragma: no cover + warnings.warn( + "cn_in_san: Support for this flag has been removed.", + RemovedInDjangoCA128Warning, + stacklevel=2, + ) self.expires = expires or ca_settings.CA_DEFAULT_EXPIRES self.add_crl_url = add_crl_url self.add_issuer_url = add_issuer_url @@ -148,7 +153,6 @@ def __eq__(self, value: object) -> bool: and self.subject == value.subject and algo and self.extensions == value.extensions - and self.cn_in_san == value.cn_in_san and self.expires == value.expires and self.add_crl_url == value.add_crl_url and self.add_issuer_url == value.add_issuer_url @@ -179,7 +183,6 @@ def create_cert( # noqa: PLR0913 expires: Expires = None, algorithm: Optional[AllowedHashTypes] = None, extensions: Optional[Iterable[x509.Extension[x509.ExtensionType]]] = None, - cn_in_san: Optional[bool] = None, add_crl_url: Optional[bool] = None, add_ocsp_url: Optional[bool] = None, add_issuer_url: Optional[bool] = None, @@ -229,9 +232,6 @@ def create_cert( # noqa: PLR0913 :py:class:`~cg:cryptography.x509.IssuerAlternativeName` extension, *add_issuer_alternative_name* is ``True`` and the passed CA has an IssuerAlternativeName set, that value will be appended to the extension you pass here. - cn_in_san : bool, optional - Override if the commonName should be added as an SubjectAlternativeName. If not passed, the value - set in the profile is used. add_crl_url : bool, optional Override if any CRL URLs from the CA should be added to the CA. If not passed, the value set in the profile is used. @@ -253,8 +253,6 @@ def create_cert( # noqa: PLR0913 The signed certificate. """ # Get overrides values from profile if not passed as parameter - if cn_in_san is None: - cn_in_san = self.cn_in_san if add_crl_url is None: add_crl_url = self.add_crl_url if add_ocsp_url is None: @@ -302,9 +300,6 @@ def create_cert( # noqa: PLR0913 # Make sure that expires is a fixed timestamp expires = self.get_expires(expires) - # Finally, add the commonName as a subjectAlternativeName if not already present. - self._update_san_from_cn(cn_in_san, subject=subject, extensions=cert_extensions) - if not subject.get_attributes_for_oid(NameOID.COMMON_NAME) and not cert_extensions.get( ExtensionOID.SUBJECT_ALTERNATIVE_NAME ): @@ -387,7 +382,6 @@ def serialize(self) -> SerializedProfile: return { "name": self.name, - "cn_in_san": self.cn_in_san, "description": self.description, "subject": subject, "algorithm": algorithm, @@ -527,40 +521,6 @@ def _update_cn_from_san( return subject - def _update_san_from_cn(self, cn_in_san: bool, subject: x509.Name, extensions: ExtensionMapping) -> None: - if cn_in_san is False: - return - - if not (common_name_attributes := subject.get_attributes_for_oid(NameOID.COMMON_NAME)): - return - - common_name_value = typing.cast(str, common_name_attributes[0].value) - try: - common_name = parse_general_name(common_name_value) - except ValueError as ex: - raise ValueError( - f"{common_name_value}: Could not parse CommonName as subjectAlternativeName." - ) from ex - - if ExtensionOID.SUBJECT_ALTERNATIVE_NAME in extensions: - san_ext = typing.cast( - x509.Extension[x509.SubjectAlternativeName], - extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME], - ) - - if common_name not in san_ext.value: - extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME] = x509.Extension( - oid=ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - critical=san_ext.critical, - value=x509.SubjectAlternativeName([*san_ext.value, common_name]), - ) - else: - extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME] = x509.Extension( - oid=ExtensionOID.SUBJECT_ALTERNATIVE_NAME, - critical=False, - value=x509.SubjectAlternativeName([common_name]), - ) - def get_profile(name: Optional[str] = None) -> Profile: """Get profile by the given name. diff --git a/ca/django_ca/static/django_ca/admin/js/profilewidget.js b/ca/django_ca/static/django_ca/admin/js/profilewidget.js index 8439cf44d..0b33047f9 100644 --- a/ca/django_ca/static/django_ca/admin/js/profilewidget.js +++ b/ca/django_ca/static/django_ca/admin/js/profilewidget.js @@ -32,10 +32,6 @@ document.addEventListener('DOMContentLoaded', function() { help.textContent = ""; // profiles don't need to have a description } - // set whether to include the CommonName in the subjectAltName - cn_in_san = document.querySelector('.field-subject_alternative_name .labeled-checkbox input'); - cn_in_san.checked = typeof profile.cn_in_san === 'undefined' || profile.cn_in_san; - // Finally, update extensions: await update_extensions(profile.extensions); clear_extensions(profile.clear_extensions); diff --git a/ca/django_ca/tasks.py b/ca/django_ca/tasks.py index 7453059ad..712e1d470 100644 --- a/ca/django_ca/tasks.py +++ b/ca/django_ca/tasks.py @@ -201,7 +201,6 @@ def sign_certificate( algorithm=message.get_algorithm(), expires=message.expires, extensions=parsed_extensions, - cn_in_san=False, ) # Store certificate in database diff --git a/ca/django_ca/tests/admin/test_actions.py b/ca/django_ca/tests/admin/test_actions.py index 369d06feb..572f19f3e 100644 --- a/ca/django_ca/tests/admin/test_actions.py +++ b/ca/django_ca/tests/admin/test_actions.py @@ -21,6 +21,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union from unittest import mock +from cryptography import x509 from cryptography.x509.oid import ExtendedKeyUsageOID, ExtensionOID, NameOID from django.contrib.auth.models import Permission @@ -40,6 +41,7 @@ from django_ca.tests.base.mixins import AdminTestCaseMixin from django_ca.tests.base.typehints import DjangoCAModelTypeVar from django_ca.tests.base.utils import override_tmpcadir +from django_ca.utils import format_general_name if typing.TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as HttpResponse @@ -413,10 +415,13 @@ def assertSuccessfulRequest( self.assertEqual(obj.cn, resigned.cn) self.assertEqual(obj.algorithm, resigned.algorithm) - for oid in [ExtensionOID.EXTENDED_KEY_USAGE, ExtensionOID.TLS_FEATURE]: - self.assertEqual(obj.x509_extensions.get(oid), resigned.x509_extensions.get(oid)) - - for oid in [ExtensionOID.KEY_USAGE, ExtensionOID.SUBJECT_ALTERNATIVE_NAME]: + for oid in [ + ExtensionOID.EXTENDED_KEY_USAGE, + ExtensionOID.TLS_FEATURE, + ExtensionOID.KEY_USAGE, + ExtensionOID.SUBJECT_ALTERNATIVE_NAME, + ]: + print(obj.x509_extensions.get(oid)) self.assertEqual(obj.x509_extensions.get(oid), resigned.x509_extensions.get(oid)) # Some properties are obviously *not* equal @@ -427,11 +432,16 @@ def assertSuccessfulRequest( def data(self) -> Dict[str, Any]: # type: ignore[override] """Return default data.""" # mypy override: https://github.com/python/mypy/issues/4125 + san = typing.cast( + x509.SubjectAlternativeName, + self.cert.x509_extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME].value, + ) return { "ca": self.cert.ca.pk, "profile": "webserver", "subject_0": json.dumps([{"key": NameOID.COMMON_NAME.dotted_string, "value": self.cert.cn}]), - "subject_alternative_name_1": True, + "subject_alternative_name_0": "\n".join([format_general_name(name) for name in san]), + "subject_alternative_name_1": False, "algorithm": "SHA-256", "expires": self.cert.ca.expires.strftime("%Y-%m-%d"), "key_usage_0": ["digital_signature", "key_agreement", "key_encipherment"], diff --git a/ca/django_ca/tests/admin/test_add_cert.py b/ca/django_ca/tests/admin/test_add_cert.py index 157412504..623a18f39 100644 --- a/ca/django_ca/tests/admin/test_add_cert.py +++ b/ca/django_ca/tests/admin/test_add_cert.py @@ -13,7 +13,6 @@ """Test cases for adding certificates via the admin interface.""" -import html import json from copy import deepcopy from datetime import datetime, timedelta, timezone as tz @@ -102,7 +101,7 @@ def add_cert(self, cname: str, ca: CertificateAuthority, algorithm: str = "SHA-2 {"key": NameOID.COMMON_NAME.dotted_string, "value": cname}, ] ), - "subject_alternative_name_1": True, + "subject_alternative_name_0": self.hostname, "algorithm": algorithm, "expires": ca.expires.strftime("%Y-%m-%d"), "certificate_policies_0": "1.2.3", @@ -152,7 +151,7 @@ def add_cert(self, cname: str, ca: CertificateAuthority, algorithm: str = "SHA-2 extended_key_usage(ExtendedKeyUsageOID.CLIENT_AUTH, ExtendedKeyUsageOID.SERVER_AUTH), key_usage(digital_signature=True, key_agreement=True), ocsp_no_check(), - subject_alternative_name(dns(cname)), + subject_alternative_name(dns(self.hostname)), tls_feature(x509.TLSFeatureType.status_request, x509.TLSFeatureType.status_request_v2), certificate_policies( x509.PolicyInformation( @@ -277,7 +276,6 @@ def test_empty_subject(self) -> None: "profile": "webserver", "subject_0": "", "subject_alternative_name_0": self.hostname, - "subject_alternative_name_1": True, "algorithm": "SHA-256", "expires": ca.expires.strftime("%Y-%m-%d"), "key_usage_0": [ @@ -325,7 +323,7 @@ def test_subject_with_multiple_org_units(self) -> None: {"key": NameOID.COMMON_NAME.dotted_string, "value": self.hostname}, ] ), - "subject_alternative_name_1": True, + "subject_alternative_name_0": self.hostname, "algorithm": "SHA-256", "expires": ca.expires.strftime("%Y-%m-%d"), "key_usage_0": [], @@ -481,7 +479,6 @@ def test_add_no_key_usage(self) -> None: ca = self.cas["root"] csr = CERT_DATA["root-cert"]["csr"]["pem"] cname = "test-add2.example.com" - san = "test-san.example.com" with self.assertCreateCertSignals() as (pre, post): response = self.client.post( @@ -496,8 +493,7 @@ def test_add_no_key_usage(self) -> None: {"key": NameOID.COMMON_NAME.dotted_string, "value": cname}, ] ), - "subject_alternative_name_0": san, - "subject_alternative_name_1": True, + "subject_alternative_name_0": self.hostname, "algorithm": "SHA-256", "expires": ca.expires.strftime("%Y-%m-%d"), "key_usage_0": [], @@ -532,7 +528,7 @@ def test_add_no_key_usage(self) -> None: self.assertEqual(cert.csr.pem, csr) # Some extensions are not set - self.assertExtensions(cert, [subject_alternative_name(dns(san), dns(cname))]) + self.assertExtensions(cert, [subject_alternative_name(dns(self.hostname))]) # Test that we can view the certificate response = self.client.get(cert.admin_change_url) @@ -559,7 +555,7 @@ def test_add_with_password(self) -> None: {"key": NameOID.COMMON_NAME.dotted_string, "value": cname}, ] ), - "subject_alternative_name_1": True, + "subject_alternative_name_0": self.hostname, "algorithm": "SHA-256", "expires": ca.expires.strftime("%Y-%m-%d"), "key_usage_0": [ @@ -594,7 +590,7 @@ def test_add_with_password(self) -> None: {"key": NameOID.COMMON_NAME.dotted_string, "value": cname}, ] ), - "subject_alternative_name_1": True, + "subject_alternative_name_0": self.hostname, "algorithm": "SHA-256", "expires": ca.expires.strftime("%Y-%m-%d"), "key_usage_0": [ @@ -630,7 +626,7 @@ def test_add_with_password(self) -> None: {"key": NameOID.COMMON_NAME.dotted_string, "value": cname}, ] ), - "subject_alternative_name_1": True, + "subject_alternative_name_0": self.hostname, "algorithm": "SHA-256", "expires": ca.expires.strftime("%Y-%m-%d"), "key_usage_0": [ @@ -670,14 +666,10 @@ def test_add_with_password(self) -> None: self.assertIssuer(ca, cert) self.assertAuthorityKeyIdentifier(ca, cert) - self.assertEqual( - cert.x509_extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME], - subject_alternative_name(dns(cname)), - ) self.assertExtensions( cert, [ - subject_alternative_name(dns(cname)), + subject_alternative_name(dns(self.hostname)), key_usage(digital_signature=True, key_agreement=True), extended_key_usage(ExtendedKeyUsageOID.CLIENT_AUTH, ExtendedKeyUsageOID.SERVER_AUTH), ], @@ -880,55 +872,6 @@ def test_expires_too_late(self) -> None: with self.assertRaises(Certificate.DoesNotExist): Certificate.objects.get(cn=cname) - @override_tmpcadir() - def test_invalid_cn_in_san(self) -> None: - """Test error with a CommonName that is not parsable as SubjectAlternativeName, but check "CN in SAN". - - .. seealso:: https://github.com/mathiasertl/django-ca/issues/62 - """ - cname = "Foo Bar" - error = "The CommonName cannot be parsed as general name. Either change the CommonName or do not include it." # NOQA - ca = self.cas["root"] - csr = CERT_DATA["root-cert"]["csr"]["pem"] - - with self.assertCreateCertSignals(False, False): - response = self.client.post( - self.add_url, - data={ - "csr": csr, - "ca": ca.pk, - "profile": "webserver", - "subject_0": json.dumps( - [ - {"key": NameOID.COUNTRY_NAME.dotted_string, "value": "US"}, - {"key": NameOID.COMMON_NAME.dotted_string, "value": cname}, - ] - ), - "subject_alternative_name_1": True, # cn_in_san - "algorithm": "SHA-256", - "expires": ca.expires.strftime("%Y-%m-%d"), - "key_usage_0": [ - "digital_signature", - "key_agreement", - ], - "key_usage_1": True, - "extended_key_usage_0": [ - ExtendedKeyUsageOID.CLIENT_AUTH.dotted_string, - ExtendedKeyUsageOID.SERVER_AUTH.dotted_string, - ], - "extended_key_usage_1": False, - "tls_feature_0": ["status_request", "status_request_v2"], - "tls_feature_1": False, - }, - ) - self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertIn(html.escape(error), response.content.decode("utf-8")) - self.assertFalse(response.context["adminform"].form.is_valid()) - self.assertEqual(response.context["adminform"].form.errors, {"subject_alternative_name": [error]}) - - with self.assertRaises(Certificate.DoesNotExist): - Certificate.objects.get(cn=cname) - @override_tmpcadir() def test_invalid_signature_hash_algorithm(self) -> None: """Test adding a certificate with an invalid signature hash algorithm.""" @@ -1149,7 +1092,6 @@ def assertProfile( # pylint: disable=invalid-name eku_critical: WebElement, tf_select: Select, tf_critical: WebElement, - cn_in_san: WebElement, ) -> None: """Assert that the admin form equals the given profile.""" profile = profiles[profile_name] @@ -1169,8 +1111,6 @@ def assertProfile( # pylint: disable=invalid-name self.assertCountEqual(tf_expected.get("value", []), tf_selected) self.assertEqual(tf_expected.get("critical", False), tf_critical.is_selected()) - self.assertEqual(profile.cn_in_san, cn_in_san.is_selected()) - def clear_form( self, ku_select: Select, @@ -1179,7 +1119,6 @@ def clear_form( eku_critical: WebElement, tf_select: Select, tf_critical: WebElement, - cn_in_san: WebElement, ) -> None: """Clear the form.""" try: @@ -1197,8 +1136,6 @@ def clear_form( eku_critical.click() if tf_critical.is_selected(): tf_critical.click() - if cn_in_san.is_selected(): - cn_in_san.click() @override_tmpcadir() def test_select_profile(self) -> None: @@ -1214,8 +1151,6 @@ def test_select_profile(self) -> None: tf_select = Select(self.find("select#id_tls_feature_0")) tf_critical = self.find("input#id_tls_feature_1") - cn_in_san = self.find("input#id_subject_alternative_name_1") - # test that the default profile is preselected self.assertEqual( [ca_settings.CA_DEFAULT_PROFILE], [o.get_attribute("value") for o in select.all_selected_options] @@ -1230,20 +1165,11 @@ def test_select_profile(self) -> None: eku_critical, tf_select, tf_critical, - cn_in_san, ) for option in select.options: # first, clear everything to make sure that the profile *sets* everything - self.clear_form( - ku_select, - ku_critical, - eku_select, - eku_critical, - tf_select, - tf_critical, - cn_in_san, - ) + self.clear_form(ku_select, ku_critical, eku_select, eku_critical, tf_select, tf_critical) value = option.get_attribute("value") if not value: @@ -1251,14 +1177,7 @@ def test_select_profile(self) -> None: option.click() self.assertProfile( - value, - ku_select, - ku_critical, - eku_select, - eku_critical, - tf_select, - tf_critical, - cn_in_san, + value, ku_select, ku_critical, eku_select, eku_critical, tf_select, tf_critical ) # Set all options to make sure that selected values are *unset* too @@ -1276,20 +1195,10 @@ def test_select_profile(self) -> None: eku_critical.click() if not tf_critical.is_selected(): tf_critical.click() - if not cn_in_san.is_selected(): - cn_in_san.click() # select empty element in profile select, then select profile again select.select_by_value(ca_settings.CA_DEFAULT_PROFILE) - self.clear_form( - ku_select, - ku_critical, - eku_select, - eku_critical, - tf_select, - tf_critical, - cn_in_san, - ) + self.clear_form(ku_select, ku_critical, eku_select, eku_critical, tf_select, tf_critical) option.click() # see that all the right things are selected @@ -1301,7 +1210,6 @@ def test_select_profile(self) -> None: eku_critical, tf_select, tf_critical, - cn_in_san, ) @@ -1678,7 +1586,7 @@ def test_none_extension_and_subject_alternative_name_extension(self) -> None: cert.ca.get_authority_key_identifier_extension(), basic_constraints(), crl_distribution_points(distribution_point(full_name=[uri(self.ca.crl_url)])), - subject_alternative_name(dns(self.hostname)), + subject_alternative_name(dns("example.com")), subject_key_identifier(cert), ], ) @@ -1711,6 +1619,7 @@ def test_only_ca_prefill(self) -> None: form = response.forms["certificate_form"] form["csr"] = CERT_DATA["child-cert"]["csr"]["pem"] form["subject_0"] = json.dumps([{"key": NameOID.COMMON_NAME.dotted_string, "value": cn}]) + form["subject_alternative_name_0"] = self.hostname response = form.submit().follow() self.assertEqual(response.status_code, 200) @@ -1727,7 +1636,7 @@ def test_only_ca_prefill(self) -> None: crl_distribution_points(distribution_point(full_name=[uri(self.ca.crl_url)])), self.ca.sign_certificate_policies, issuer_alternative_name(uri(self.ca.issuer_alt_name)), - subject_alternative_name(dns(cn)), + subject_alternative_name(dns(self.hostname)), subject_key_identifier(cert), ], ) @@ -1860,7 +1769,6 @@ def test_full_profile_prefill(self) -> None: ), key_usage(key_agreement=True, key_cert_sign=True), ocsp_no_check(critical=True), - subject_alternative_name(dns(cn)), subject_key_identifier(cert), tls_feature(x509.TLSFeatureType.status_request, critical=True), ], @@ -1963,7 +1871,6 @@ def test_multiple_distribution_points(self) -> None: crl_issuer=[uri("http://freshest-crl-issuer.profile.example.com")], critical=False, ), - subject_alternative_name(dns(cn)), subject_key_identifier(cert), ], ) diff --git a/ca/django_ca/tests/base/mixins.py b/ca/django_ca/tests/base/mixins.py index 786340fc3..bbb0a9152 100644 --- a/ca/django_ca/tests/base/mixins.py +++ b/ca/django_ca/tests/base/mixins.py @@ -757,34 +757,6 @@ def _read_mock(size=None): # type: ignore # pylint: disable=unused-argument return stdout.getvalue(), stderr.getvalue() - def cmd_help_text(self, cmd: str) -> str: - """Get the help message for a given management command. - - Also asserts that stderr is empty and the command exists with status code 0. - """ - stdout = io.StringIO() - stderr = io.StringIO() - with mock.patch("sys.stdout", stdout), mock.patch("sys.stderr", stderr): - util = ManagementUtility(["manage.py", cmd, "--help"]) - with self.assertSystemExit(0): - util.execute() - - self.assertEqual(stderr.getvalue(), "") - return stdout.getvalue() - - @classmethod - def create_cert( - cls, - ca: CertificateAuthority, - csr: x509.CertificateSigningRequest, - subject: Optional[x509.Name], - **kwargs: Any, - ) -> Certificate: - """Create a certificate with the given data.""" - cert = Certificate.objects.create_cert(ca, csr, subject=subject, **kwargs) - cert.full_clean() - return cert - def certificate_policies( self, *policies: x509.PolicyInformation, critical: bool = False ) -> x509.Extension[x509.CertificatePolicies]: diff --git a/ca/django_ca/tests/commands/test_sign_cert.py b/ca/django_ca/tests/commands/test_sign_cert.py index 2939515ef..301d2cb93 100644 --- a/ca/django_ca/tests/commands/test_sign_cert.py +++ b/ca/django_ca/tests/commands/test_sign_cert.py @@ -15,7 +15,6 @@ import io import os -import re import stat import unittest from datetime import timedelta @@ -94,9 +93,7 @@ def test_from_stdin(self) -> None: self.assertEqual( actual[ExtensionOID.EXTENDED_KEY_USAGE], extended_key_usage(ExtendedKeyUsageOID.SERVER_AUTH) ) - self.assertEqual( - actual[ExtensionOID.SUBJECT_ALTERNATIVE_NAME], subject_alternative_name(dns(self.hostname)) - ) + self.assertNotIn(ExtensionOID.SUBJECT_ALTERNATIVE_NAME, actual) self.assertIssuer(self.ca, cert) self.assertAuthorityKeyIdentifier(self.ca, cert) @@ -151,9 +148,6 @@ def test_usable_cas(self) -> None: self.assertEqual( actual[ExtensionOID.EXTENDED_KEY_USAGE], extended_key_usage(ExtendedKeyUsageOID.SERVER_AUTH) ) - self.assertEqual( - actual[ExtensionOID.SUBJECT_ALTERNATIVE_NAME], subject_alternative_name(dns(self.hostname)) - ) self.assertIssuer(ca, cert) self.assertAuthorityKeyIdentifier(ca, cert) @@ -188,9 +182,7 @@ def test_from_file(self) -> None: self.assertEqual( actual[ExtensionOID.EXTENDED_KEY_USAGE], extended_key_usage(ExtendedKeyUsageOID.SERVER_AUTH) ) - self.assertEqual( - actual[ExtensionOID.SUBJECT_ALTERNATIVE_NAME], subject_alternative_name(dns(self.hostname)) - ) + self.assertNotIn(ExtensionOID.SUBJECT_ALTERNATIVE_NAME, actual) @override_tmpcadir() def test_to_file(self) -> None: @@ -313,55 +305,6 @@ def test_subject_sort_with_no_common_name(self) -> None: ), ) - @override_tmpcadir() - def test_no_dns_cn(self) -> None: - """Test using a CN that is not a valid DNS name.""" - # Use a CommonName that is *not* a valid DNSName. By default, this is added as a subjectAltName, which - # should fail. - - stdin = self.csr_pem.encode() - cname = "foo bar" - msg = rf"^{cname}: Could not parse CommonName as subjectAlternativeName\.$" - subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cname)]).rfc4514_string() - - with self.assertCommandError(msg), self.assertCreateCertSignals(False, False): - self.cmd( - "sign_cert", - ca=self.ca, - subject=subject, - subject_format="rfc4514", - cn_in_san=True, - stdin=stdin, - ) - - @override_tmpcadir() - def test_cn_not_in_san(self) -> None: - """Test adding a CN that is not in the SAN.""" - stdin = self.csr_pem.encode() - with self.assertCreateCertSignals() as (pre, post): - stdout, stderr = self.cmd( - "sign_cert", - ca=self.ca, - subject=self.subject.rfc4514_string(), - subject_format="rfc4514", - cn_in_san=False, - subject_alternative_name=subject_alternative_name(dns("example.com")).value, - stdin=stdin, - ) - - cert = Certificate.objects.get() - self.assertPostIssueCert(post, cert) - self.assertSignature([self.ca], cert) - self.assertIssuer(self.ca, cert) - self.assertAuthorityKeyIdentifier(self.ca, cert) - self.assertEqual(cert.pub.loaded.subject, self.subject) - self.assertEqual(stdout, f"Please paste the CSR:\n{cert.pub.pem}") - self.assertEqual(stderr, "") - self.assertEqual( - cert.x509_extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME], - subject_alternative_name(dns("example.com")), - ) - @override_tmpcadir() def test_no_san(self) -> None: """Test signing without passing any SANs.""" @@ -372,7 +315,6 @@ def test_no_san(self) -> None: ca=self.ca, subject=self.subject.rfc4514_string(), subject_format="rfc4514", - cn_in_san=False, stdin=stdin, ) @@ -401,13 +343,10 @@ def test_profile_subject(self) -> None: """Test signing with a subject in the profile.""" # first, we only pass an subjectAltName, meaning that even the CommonName is used. stdin = self.csr_pem.encode() + san = subject_alternative_name(dns(self.hostname)) with self.assertCreateCertSignals() as (pre, post): stdout, stderr = self.cmd( - "sign_cert", - ca=self.ca, - cn_in_san=False, - subject_alternative_name=subject_alternative_name(dns(self.hostname)).value, - stdin=stdin, + "sign_cert", ca=self.ca, subject_alternative_name=san.value, stdin=stdin ) self.assertEqual(stderr, "") @@ -418,6 +357,7 @@ def test_profile_subject(self) -> None: self.assertIssuer(self.ca, cert) self.assertAuthorityKeyIdentifier(self.ca, cert) self.assertEqual(stdout, f"Please paste the CSR:\n{cert.pub.pem}") + self.assertEqual(cert.x509_extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME], san) # replace subject fields via command-line argument: subject = x509.Name( @@ -435,8 +375,7 @@ def test_profile_subject(self) -> None: self.cmd( "sign_cert", ca=self.ca, - cn_in_san=False, - subject_alternative_name=subject_alternative_name(dns(self.hostname)).value, + subject_alternative_name=san.value, stdin=stdin, subject=subject.rfc4514_string(), subject_format="rfc4514", @@ -445,6 +384,7 @@ def test_profile_subject(self) -> None: cert = Certificate.objects.get(cn="CommonName2") self.assertPostIssueCert(post, cert) self.assertEqual(cert.pub.loaded.subject, subject) + self.assertEqual(cert.x509_extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME], san) @override_tmpcadir() def test_extensions(self) -> None: @@ -549,8 +489,7 @@ def test_extensions(self) -> None: # Test Subject Alternative Name extension self.assertEqual( - extensions[x509.SubjectAlternativeName.oid], - subject_alternative_name(uri("https://example.net"), dns(self.hostname)), + extensions[x509.SubjectAlternativeName.oid], subject_alternative_name(uri("https://example.net")) ) # Test TLSFeature extension @@ -648,7 +587,7 @@ def test_extensions_with_non_default_critical(self) -> None: # Test Subject Alternative Name extension (NOTE: Common Name is automatically appended). self.assertEqual( cert.x509_extensions[x509.SubjectAlternativeName.oid], - subject_alternative_name(uri("https://example.net"), dns(self.hostname), critical=True), + subject_alternative_name(uri("https://example.net"), critical=True), ) @override_tmpcadir(CA_MIN_KEY_SIZE=1024) @@ -723,7 +662,7 @@ def test_multiple_sans(self) -> None: self.assertEqual(stdout, f"Please paste the CSR:\n{cert.pub.pem}") self.assertEqual( cert.x509_extensions[x509.SubjectAlternativeName.oid], - subject_alternative_name(uri("https://example.net"), dns("example.org"), dns(self.hostname)), + subject_alternative_name(uri("https://example.net"), dns("example.org")), ) @override_tmpcadir(CA_DEFAULT_SUBJECT=tuple()) @@ -828,9 +767,6 @@ def test_der_csr(self) -> None: self.assertEqual( actual[ExtensionOID.EXTENDED_KEY_USAGE], extended_key_usage(ExtendedKeyUsageOID.SERVER_AUTH) ) - self.assertEqual( - actual[ExtensionOID.SUBJECT_ALTERNATIVE_NAME], subject_alternative_name(dns(self.hostname)) - ) @override_tmpcadir(CA_DEFAULT_SUBJECT=None) def test_unsortable_subject_with_no_profile_subject(self) -> None: @@ -982,27 +918,6 @@ def test_expired_ca(self) -> None: "sign_cert", ca=self.ca, subject_format="rfc4514", subject=f"CN={self.hostname}", stdin=stdin ) - @override_tmpcadir() - def test_help_text(self) -> None: - """Test the help text.""" - with self.assertCreateCertSignals(False, False): - help_text = self.cmd_help_text("sign_cert") - - # Remove newlines and multiple spaces from text for matching independent of terminal width - help_text = re.sub(r"\s+", " ", help_text.replace("\n", "")) - - self.assertIn("Do not add the CommonName as subjectAlternativeName.", help_text) - self.assertIn("Add the CommonName as subjectAlternativeName (default).", help_text) - - with self.assertCreateCertSignals(False, False), self.settings( - CA_PROFILES={"webserver": {"cn_in_san": False}} - ): - help_text = self.cmd_help_text("sign_cert") - help_text = re.sub(r"\s+", " ", help_text.replace("\n", "")) - - self.assertIn("Do not add the CommonName as subjectAlternativeName (default).", help_text) - self.assertIn("Add the CommonName as subjectAlternativeName.", help_text) - @override_tmpcadir() def test_add_any_policy(self) -> None: """Test adding the anyPolicy, which is an error for end-entity certificates.""" diff --git a/ca/django_ca/tests/test_managers.py b/ca/django_ca/tests/test_managers.py index 1d7052afa..66d7b6f85 100644 --- a/ca/django_ca/tests/test_managers.py +++ b/ca/django_ca/tests/test_managers.py @@ -44,7 +44,6 @@ ocsp_no_check, override_tmpcadir, precert_poison, - subject_alternative_name, tls_feature, uri, ) @@ -451,7 +450,7 @@ def test_basic(self) -> None: with self.assertCreateCertSignals(): cert = Certificate.objects.create_cert(self.ca, self.csr, subject=self.subject) self.assertEqual(cert.subject, self.subject) - self.assertExtensions(cert, [subject_alternative_name(dns(self.hostname))]) + self.assertExtensions(cert, []) @override_tmpcadir(CA_PROFILES={ca_settings.CA_DEFAULT_PROFILE: {"extensions": {}}}) def test_explicit_profile(self) -> None: @@ -461,7 +460,7 @@ def test_explicit_profile(self) -> None: self.ca, self.csr, subject=self.subject, profile=profiles[ca_settings.CA_DEFAULT_PROFILE] ) self.assertEqual(cert.subject, self.subject) - self.assertExtensions(cert, [subject_alternative_name(dns(self.hostname))]) + self.assertExtensions(cert, []) @override_tmpcadir() def test_cryptography_extensions(self) -> None: @@ -475,7 +474,6 @@ def test_cryptography_extensions(self) -> None: self.assertExtensions( cert, [ - subject_alternative_name(dns(self.hostname)), expected_key_usage, extended_key_usage(ExtendedKeyUsageOID.SERVER_AUTH), ], diff --git a/ca/django_ca/tests/test_models.py b/ca/django_ca/tests/test_models.py index 6dcf3ad60..94cac1da1 100644 --- a/ca/django_ca/tests/test_models.py +++ b/ca/django_ca/tests/test_models.py @@ -13,7 +13,6 @@ """Test Django model classes.""" -import ipaddress import json import os import re @@ -67,10 +66,8 @@ basic_constraints, crl_distribution_points, distribution_point, - dns, issuer_alternative_name, override_tmpcadir, - subject_alternative_name, subject_key_identifier, uri, ) @@ -984,7 +981,6 @@ def test_simple(self) -> None: cert, [ subject_key_identifier(cert), - subject_alternative_name(dns(cn)), basic_constraints(), self.ca.get_authority_key_identifier_extension(), ], @@ -1028,103 +1024,12 @@ def test_non_default_extensions(self) -> None: with self.assertSignCertSignals(): cert = self.ca.sign( - csr, - subject=subject, - cn_in_san=False, - extensions=[basic_constraints(critical=False), ski, aki], + csr, subject=subject, extensions=[basic_constraints(critical=False), ski, aki] ) self.assertBasicCert(cert) self.assertExtensionDict(cert, [ski, basic_constraints(critical=False), aki]) - @override_tmpcadir() - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_cn_not_in_san(self) -> None: - """Test the cn_in_san option.""" - cn = "example.com" - csr = CERT_DATA["child-cert"]["csr"]["parsed"] - subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]) - san = subject_alternative_name(dns("example.net")) - with self.assertSignCertSignals(): - cert = self.ca.sign(csr, subject=subject, cn_in_san=False, extensions=[san]) - - self.assertBasicCert(cert) - self.assertExtensionDict( - cert, - [ - subject_key_identifier(cert), - san, - basic_constraints(), - self.ca.get_authority_key_identifier_extension(), - ], - ) - - @override_tmpcadir() - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_append_cn_to_san(self) -> None: - """Test appending a CommonName to SubjectAlternativeName.""" - cn = "example.com" - csr = CERT_DATA["child-cert"]["csr"]["parsed"] - subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]) - san = subject_alternative_name(dns("example.net")) - with self.assertSignCertSignals(): - cert = self.ca.sign(csr, subject=subject, cn_in_san=True, extensions=[san]) - - self.assertBasicCert(cert) - self.assertExtensionDict( - cert, - [ - subject_key_identifier(cert), - subject_alternative_name(dns("example.net"), dns(cn)), - basic_constraints(), - self.ca.get_authority_key_identifier_extension(), - ], - ) - - @override_tmpcadir() - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_cn_already_in_san(self) -> None: - """Test using a CommonName that is already in SubjectAlternativeName.""" - cn = "example.com" - csr = CERT_DATA["child-cert"]["csr"]["parsed"] - subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]) - san = subject_alternative_name(dns(cn)) - with self.assertSignCertSignals(): - cert = self.ca.sign(csr, subject=subject, cn_in_san=True, extensions=[san]) - - self.assertBasicCert(cert) - self.assertExtensionDict( - cert, - [ - subject_key_identifier(cert), - san, - basic_constraints(), - self.ca.get_authority_key_identifier_extension(), - ], - ) - - @override_tmpcadir() - @freeze_time(TIMESTAMPS["everything_valid"]) - def test_unparsable_cn(self) -> None: - """Test using a CommonName that cannot be used as a SubjectAlternativeName.""" - cn = "foo..bar*" - csr = CERT_DATA["child-cert"]["csr"]["parsed"] - subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]) - san = subject_alternative_name(dns("example.net")) - with self.assertSignCertSignals(): - cert = self.ca.sign(csr, subject=subject, cn_in_san=True, extensions=[san]) - - self.assertBasicCert(cert) - self.assertExtensionDict( - cert, - [ - subject_key_identifier(cert), - san, - basic_constraints(), - self.ca.get_authority_key_identifier_extension(), - ], - ) - def test_create_ca(self) -> None: """Try passing a BasicConstraints extension that allows creating a CA.""" csr = CERT_DATA["child-cert"]["csr"]["parsed"] @@ -1196,54 +1101,6 @@ def test_serial(self) -> None: for name, cert in self.certs.items(): self.assertEqual(cert.serial, CERT_DATA[name].get("serial")) - @override_tmpcadir() - def test_subject_alternative_name(self) -> None: - """Test getting the subjectAlternativeName extension.""" - for name, ca in self.cas.items(): - self.assertEqual( - ca.x509_extensions.get(ExtensionOID.SUBJECT_ALTERNATIVE_NAME), - CERT_DATA[name].get("subject_alternative_name"), - ) - - for name, cert in self.certs.items(): - self.assertEqual( - cert.x509_extensions.get(ExtensionOID.SUBJECT_ALTERNATIVE_NAME), - CERT_DATA[name].get("subject_alternative_name"), - ) - - # Directory names are almost never used in SubjectAlternativeName - directory_name = x509.DirectoryName( - x509.Name( - [ - x509.NameAttribute(oid=NameOID.COUNTRY_NAME, value="AT"), - x509.NameAttribute(oid=NameOID.COMMON_NAME, value="example.com"), - ] - ) - ) - - # Create a cert with some weirder SANs to test that too - san = subject_alternative_name( - directory_name, - x509.RFC822Name("user@example.com"), - x509.IPAddress(ipaddress.IPv6Address("fd00::1")), - ) - weird_cert = self.create_cert( - self.cas["child"], - CERT_DATA["child-cert"]["csr"]["parsed"], - subject=self.subject, - extensions=[san], - ) - - expected_san = subject_alternative_name( - directory_name, - x509.RFC822Name("user@example.com"), - x509.IPAddress(ipaddress.IPv6Address("fd00::1")), - dns("test-models.certificatetests.test-subject-alternative-name.example.com"), - ) - actual_san = weird_cert.x509_extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME] - self.assertEqual(expected_san.critical, actual_san.critical) - self.assertEqual(weird_cert.x509_extensions[ExtensionOID.SUBJECT_ALTERNATIVE_NAME], expected_san) - @freeze_time("2019-02-03 15:43:12") def test_get_revocation_time(self) -> None: """Test getting the revocation time.""" diff --git a/ca/django_ca/tests/test_profiles.py b/ca/django_ca/tests/test_profiles.py index 7343c232a..67fb3f48f 100644 --- a/ca/django_ca/tests/test_profiles.py +++ b/ca/django_ca/tests/test_profiles.py @@ -84,9 +84,7 @@ def test_python_intro(self) -> None: class ProfileTestCase(TestCaseMixin, TestCase): """Main tests for the profile class.""" - def create_cert( # type: ignore[override] - self, prof: Profile, ca: CertificateAuthority, *args: Any, **kwargs: Any - ) -> Certificate: + def create_cert(self, prof: Profile, ca: CertificateAuthority, *args: Any, **kwargs: Any) -> Certificate: """Shortcut to create a cert with the given profile.""" cert = Certificate(ca=ca) cert.update_certificate(prof.create_cert(ca, *args, **kwargs)) @@ -108,10 +106,7 @@ def test_create_cert_minimal(self) -> None: add_issuer_alternative_name=False, ) self.assertEqual(pre.call_count, 1) - self.assertExtensions( - cert, - [ca.get_authority_key_identifier_extension(), subject_alternative_name(dns(self.hostname))], - ) + self.assertExtensions(cert, [ca.get_authority_key_identifier_extension()]) @override_tmpcadir() def test_alternative_values(self) -> None: @@ -175,7 +170,6 @@ def test_overrides(self) -> None: subject_key_identifier(cert), ca.get_authority_key_identifier_extension(), basic_constraints(), - subject_alternative_name(dns(self.hostname)), ], expect_defaults=False, ) @@ -199,7 +193,6 @@ def test_overrides(self) -> None: [ ca.get_authority_key_identifier_extension(), basic_constraints(), - subject_alternative_name(dns(self.hostname)), ], ) @@ -215,78 +208,6 @@ def test_none_extension(self) -> None: self.assertEqual(pre.call_count, 1) self.assertNotIn(ExtensionOID.OCSP_NO_CHECK, cert.x509_extensions) - @override_tmpcadir() - def test_cn_in_san(self) -> None: - """Test writing the common name into the SAN.""" - ca = self.load_ca(name="root", parsed=CERT_DATA["root"]["pub"]["parsed"]) - csr = CERT_DATA["child-cert"]["csr"]["parsed"] - - prof = Profile("example", subject=False, add_issuer_alternative_name=False, cn_in_san=False) - with self.mockSignal(pre_sign_cert) as pre: - cert = self.create_cert(prof, ca, csr, subject=self.subject) - self.assertEqual(pre.call_count, 1) - self.assertEqual(cert.subject, self.subject) - self.assertExtensions(cert, [ca.get_authority_key_identifier_extension()]) - - # Create the same cert, but pass cn_in_san=True to create_cert - with self.mockSignal(pre_sign_cert) as pre: - cert = self.create_cert(prof, ca, csr, subject=self.subject, cn_in_san=True) - self.assertEqual(pre.call_count, 1) - self.assertEqual(cert.subject, self.subject) - self.assertExtensions( - cert, - [ca.get_authority_key_identifier_extension(), subject_alternative_name(dns(self.hostname))], - ) - - # test that cn_in_san=True with a SAN that already contains the CN does not lead to a duplicate - with self.mockSignal(pre_sign_cert) as pre: - cert = self.create_cert( - prof, - ca, - csr, - subject=self.subject, - cn_in_san=True, - extensions=[subject_alternative_name(dns(self.hostname))], - ) - self.assertEqual(pre.call_count, 1) - self.assertEqual(cert.subject, self.subject) - self.assertExtensions( - cert, - [ca.get_authority_key_identifier_extension(), subject_alternative_name(dns(self.hostname))], - ) - - # test that cn_in_san=True with a SAN that does NOT yet contain the CN, so it's added - with self.mockSignal(pre_sign_cert) as pre: - cert = self.create_cert( - prof, - ca, - csr, - subject=self.subject, - cn_in_san=True, - extensions=[subject_alternative_name(dns(self.hostname + ".added"))], - ) - self.assertEqual(pre.call_count, 1) - self.assertEqual(cert.subject, self.subject) - self.assertExtensions( - cert, - [ - ca.get_authority_key_identifier_extension(), - subject_alternative_name(dns(self.hostname + ".added"), dns(self.hostname)), - ], - ) - - # test that the first SAN is added as CN if we don't have A CN - with self.mockSignal(pre_sign_cert) as pre: - cert = self.create_cert( - prof, ca, csr, cn_in_san=True, extensions=[subject_alternative_name(dns(self.hostname))] - ) - self.assertEqual(pre.call_count, 1) - self.assertEqual(cert.subject, self.subject) - self.assertExtensions( - cert, - [ca.get_authority_key_identifier_extension(), subject_alternative_name(dns(self.hostname))], - ) - @override_tmpcadir() def test_override_ski(self) -> None: """Test overriding the subject key identifier.""" @@ -314,12 +235,7 @@ def test_override_ski(self) -> None: self.assertEqual(pre.call_count, 1) self.assertExtensions( cert, - [ - ca.get_authority_key_identifier_extension(), - basic_constraints(), - subject_alternative_name(dns(self.hostname)), - ski, - ], + [ca.get_authority_key_identifier_extension(), basic_constraints(), ski], expect_defaults=False, ) @@ -357,7 +273,6 @@ def test_add_distribution_point_with_ca_crldp(self) -> None: ca.get_authority_key_identifier_extension(), basic_constraints(), x509.Extension(oid=ExtensionOID.SUBJECT_KEY_IDENTIFIER, critical=False, value=ski), - subject_alternative_name(dns(self.hostname)), added_crldp, ], expect_defaults=False, @@ -422,7 +337,6 @@ def test_issuer_alternative_name_override(self) -> None: ca.get_authority_key_identifier_extension(), basic_constraints(), x509.Extension(oid=ExtensionOID.SUBJECT_KEY_IDENTIFIER, critical=False, value=ski), - subject_alternative_name(dns(self.hostname)), issuer_alternative_name(added_ian_uri), ], expect_defaults=False, @@ -468,7 +382,6 @@ def test_merge_authority_information_access_existing_values(self) -> None: ca.get_authority_key_identifier_extension(), basic_constraints(), x509.Extension(oid=ExtensionOID.SUBJECT_KEY_IDENTIFIER, critical=False, value=ski), - subject_alternative_name(dns(self.hostname)), authority_information_access( ca_issuers=[cert_issuers, cert_issuers2], ocsp=[cert_ocsp], @@ -496,12 +409,7 @@ def test_extension_as_cryptography(self) -> None: self.assertEqual(pre.call_count, 1) self.assertExtensions( cert, - [ - ca.get_authority_key_identifier_extension(), - basic_constraints(), - ocsp_no_check(), - subject_alternative_name(dns(self.hostname)), - ], + [ca.get_authority_key_identifier_extension(), basic_constraints(), ocsp_no_check()], ) @override_tmpcadir() @@ -643,22 +551,9 @@ def test_no_valid_cn_in_san(self) -> None: san = subject_alternative_name(x509.RegisteredID(ExtensionOID.OCSP_NO_CHECK)) with self.mockSignal(pre_sign_cert) as pre: - self.create_cert(prof, ca, csr, cn_in_san=True, extensions=[san]) + self.create_cert(prof, ca, csr, extensions=[san]) self.assertEqual(pre.call_count, 1) - @override_tmpcadir() - def test_unparsable_cn(self) -> None: - """Try creating a profile with an unparsable Common Name.""" - ca = self.load_ca(name="root", parsed=CERT_DATA["root"]["pub"]["parsed"]) - csr = CERT_DATA["child-cert"]["csr"]["parsed"] - cname = "foo bar" - - prof = Profile("example", subject=x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, cname)])) - msg = rf"^{cname}: Could not parse CommonName as subjectAlternativeName\.$" - with self.mockSignal(pre_sign_cert) as pre, self.assertRaisesRegex(ValueError, msg): - self.create_cert(prof, ca, csr) - self.assertEqual(pre.call_count, 0) - def test_unknown_signature_hash_algorithm(self) -> None: """Test passing an unknown hash algorithm.""" with self.assertRaisesRegex(ValueError, r"^foo: Unknown hash algorithm\.$"): @@ -809,7 +704,6 @@ def test_serialize() -> None: key_usage = ["digital_signature"] prof = Profile( "test", - cn_in_san=True, algorithm="SHA-512", description=desc, subject=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "example.com")]), @@ -821,7 +715,6 @@ def test_serialize() -> None: assert prof.serialize() == { "name": "test", "algorithm": "SHA-512", - "cn_in_san": True, "subject": [{"oid": NameOID.COMMON_NAME.dotted_string, "value": "example.com"}], "description": desc, "clear_extensions": ["extended_key_usage"], diff --git a/ca/django_ca/typehints.py b/ca/django_ca/typehints.py index cefaf12a0..224192cc9 100644 --- a/ca/django_ca/typehints.py +++ b/ca/django_ca/typehints.py @@ -295,7 +295,6 @@ def __lt__(self, __other: Any) -> bool: # pragma: nocover "description": str, "subject": Optional[SerializedPydanticName], "algorithm": Optional[HashAlgorithms], - "cn_in_san": bool, "extensions": List[SerializedPydanticExtension], "clear_extensions": List[str], }, diff --git a/ca/django_ca/widgets.py b/ca/django_ca/widgets.py index 568decf46..e4dfd0294 100644 --- a/ca/django_ca/widgets.py +++ b/ca/django_ca/widgets.py @@ -26,7 +26,7 @@ from django_ca import ca_settings from django_ca.constants import EXTENSION_DEFAULT_CRITICAL, KEY_USAGE_NAMES, REVOCATION_REASONS from django_ca.extensions.utils import certificate_policies_is_simple -from django_ca.typehints import KeyUsages +from django_ca.typehints import AlternativeNameTypeVar, KeyUsages from django_ca.utils import format_general_name log = logging.getLogger(__name__) @@ -283,6 +283,19 @@ def get_widgets(self, **kwargs: Any) -> ExtensionWidgetsType: ) +class AlternativeNameWidget(ExtensionWidget, typing.Generic[AlternativeNameTypeVar]): + """Widget for a :py:class:`~cg:cryptography.x509.IssuerAlternativeName` extension.""" + + extension_widgets = (GeneralNamesWidget(attrs={"rows": 3}),) + + def decompress( + self, value: Optional[x509.Extension[AlternativeNameTypeVar]] + ) -> Tuple[List[x509.GeneralName], bool]: + if value is None: + return [], EXTENSION_DEFAULT_CRITICAL[self.oid] + return list(value.value), value.critical + + class DistributionPointWidget(ExtensionWidget): """Widgets for extensions that use a DistributionPoint.""" @@ -476,19 +489,11 @@ def decompress(self, value: Optional[x509.Extension[x509.KeyUsage]]) -> Tuple[Li return choices, value.critical -class IssuerAlternativeNameWidget(ExtensionWidget): +class IssuerAlternativeNameWidget(AlternativeNameWidget[x509.IssuerAlternativeName]): """Widget for a :py:class:`~cg:cryptography.x509.IssuerAlternativeName` extension.""" - extension_widgets = (GeneralNamesWidget(attrs={"rows": 3}),) oid = ExtensionOID.ISSUER_ALTERNATIVE_NAME - def decompress( - self, value: Optional[x509.Extension[x509.IssuerAlternativeName]] - ) -> Tuple[List[x509.GeneralName], bool]: - if value is None: - return [], EXTENSION_DEFAULT_CRITICAL[self.oid] - return list(value.value), value.critical - class OCSPNoCheckWidget(ExtensionWidget): """Widget for a :py:class:`~cg:cryptography.x509.OCSPNoCheck` extension.""" @@ -502,39 +507,11 @@ def decompress(self, value: Optional[x509.Extension[x509.OCSPNoCheck]]) -> Tuple return True, value.critical -class SubjectAlternativeNameWidget(ExtensionWidget): +class SubjectAlternativeNameWidget(AlternativeNameWidget[x509.SubjectAlternativeName]): """Widget for a :py:class:`~cg:cryptography.x509.IssuerAlternativeName` extension.""" - extension_widgets = ( - GeneralNamesWidget(attrs={"rows": 3}), - LabeledCheckboxInput(label="Include CommonName"), - ) oid = ExtensionOID.SUBJECT_ALTERNATIVE_NAME - # COVERAGE NOTE: In Django 4.1, decompress is not called if compress() returns a tuple - # https://github.com/django/django/commit/37602e49484a88867f40e9498f86c49c2d1c5d7c - def decompress( - self, - value: Optional[ - Union[ - Tuple[List[x509.GeneralName], bool, bool], - Tuple[x509.Extension[x509.SubjectAlternativeName], bool], - ] - ], - ) -> Tuple[List[x509.GeneralName], bool, bool]: # pragma: no cover - if value is None: - default_cn_in_san = ca_settings.CA_PROFILES[ca_settings.CA_DEFAULT_PROFILE]["cn_in_san"] - return [], default_cn_in_san, EXTENSION_DEFAULT_CRITICAL[self.oid] - - if len(value) == 3: - return value - - ext, cn_in_san = value - if ext is None: - return [], cn_in_san, EXTENSION_DEFAULT_CRITICAL[self.oid] - - return list(ext.value), cn_in_san, ext.critical - class TLSFeatureWidget(MultipleChoiceExtensionWidget): """Widget for a :py:class:`~cg:cryptography.x509.TLSFeature` extension.""" diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 0029fa76e..d3b9d6adb 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -16,6 +16,8 @@ ChangeLog :ref:`update_126_rfc4514_subjects` for migration information. * Add support for ``Django~=5.0`` and ``acme==2.8.0``. +* Remove the unpredictable :command:`manage.py sign_cert` ``--cn-in-san`` option. List all required names for + the Subject Alternative Name instead. * ``pydantic>=2.5`` is now a required dependency. * Add :doc:`Pydantic models for cryptography classes `. These are required for the REST API, but are also used internally for various places where serialization of objects is required. diff --git a/docs/source/profiles.rst b/docs/source/profiles.rst index bd6bcaffd..f7390290c 100644 --- a/docs/source/profiles.rst +++ b/docs/source/profiles.rst @@ -123,7 +123,6 @@ Option Default Description keys. ``autogenerated`` ``False`` Set to ``True`` if you want to mark certificates from this profile as automatically generated by default. -``cn_in_san`` ``True`` If the CommonName should be added as Subject Alternative Name. ``description`` ``''`` Informal text explaining what the profile is. ``expires`` A ``timedelta`` of when a certificate will expire, if you set an integer it will be interpreted as a number of days. This defaults to