diff --git a/ca/django_ca/constants.py b/ca/django_ca/constants.py index c195f5547..b1feb08e7 100644 --- a/ca/django_ca/constants.py +++ b/ca/django_ca/constants.py @@ -36,7 +36,14 @@ # IMPORTANT: Do **not** import any module from django_ca at runtime here, or you risk circular imports. if typing.TYPE_CHECKING: - from django_ca.typehints import AccessMethods, AllowedHashTypes, HashAlgorithms, KeyUsages, OtherNames + from django_ca.typehints import ( + AccessMethods, + AllowedHashTypes, + GeneralNames, + HashAlgorithms, + KeyUsages, + OtherNames, + ) ACCESS_METHOD_TYPES: "MappingProxyType[AccessMethods, x509.ObjectIdentifier]" = MappingProxyType( { @@ -282,8 +289,7 @@ class ExtendedKeyUsageOID(_ExtendedKeyUsageOID): ) #: Map for types of general names. -# TODO: test completeness -GENERAL_NAME_TYPES: "MappingProxyType[str, Type[x509.GeneralName]]" = MappingProxyType( +GENERAL_NAME_TYPES: "MappingProxyType[GeneralNames, Type[x509.GeneralName]]" = MappingProxyType( { "email": x509.RFC822Name, "URI": x509.UniformResourceIdentifier, @@ -294,7 +300,7 @@ class ExtendedKeyUsageOID(_ExtendedKeyUsageOID): "otherName": x509.OtherName, } ) -GENERAL_NAME_NAMES: "MappingProxyType[Type[x509.GeneralName], str]" = MappingProxyType( +GENERAL_NAME_NAMES: "MappingProxyType[Type[x509.GeneralName], GeneralNames]" = MappingProxyType( {v: k for k, v in GENERAL_NAME_TYPES.items()} ) diff --git a/ca/django_ca/tests/test_constants.py b/ca/django_ca/tests/test_constants.py index 12c0f74b5..0e95a2942 100644 --- a/ca/django_ca/tests/test_constants.py +++ b/ca/django_ca/tests/test_constants.py @@ -14,7 +14,7 @@ """Some sanity tests for constants.""" import typing -from typing import Set, Type +from typing import Any, Set, Type from cryptography import x509 from cryptography.hazmat.primitives import hashes @@ -24,8 +24,9 @@ from django.test import TestCase from django_ca import constants +from django_ca.typehints import GeneralNames, HashAlgorithms -SuperclassTypeVar = typing.TypeVar("SuperclassTypeVar", bound=Type[object]) +SuperclassTypeVar = typing.TypeVar("SuperclassTypeVar", bound=Type[Any]) KNOWN_EXTENSION_OIDS = list( filter( lambda attr: isinstance(attr, x509.ObjectIdentifier), @@ -40,6 +41,53 @@ ) +def get_subclasses(cls: Type[SuperclassTypeVar]) -> Set[Type[SuperclassTypeVar]]: + """Recursively get a list of subclasses. + + .. seealso:: https://stackoverflow.com/a/3862957 + """ + return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in get_subclasses(c)]) + + +def test_general_name_types() -> None: + """Test :py:attr:`~django_ca.constants.GENERAL_NAME_TYPES` for completeness.""" + subclasses = get_subclasses(x509.GeneralName) # type: ignore[type-var, type-abstract] + assert len(constants.GENERAL_NAME_TYPES) == len(subclasses) + assert set(constants.GENERAL_NAME_TYPES.values()) == set(subclasses) + + # Make sure that keys match the typehint exactly + assert sorted(constants.GENERAL_NAME_TYPES) == sorted(typing.get_args(GeneralNames)) + + +def test_hash_algorithm_names() -> None: + """Test :py:attr:`~django_ca.constants.GENERAL_NAME_TYPES` for completeness.""" + subclasses = get_subclasses(hashes.HashAlgorithm) # type: ignore[type-var, type-abstract] + + # filter out hash algorithms that are not supported right now due to them having a digest size as + # parameter + excluded_algorithms = ( + hashes.SHAKE128, + hashes.SHAKE256, + hashes.BLAKE2b, + hashes.BLAKE2s, + hashes.SM3, + hashes.SHA512_224, + hashes.SHA512_256, + ) + subclasses = set(sc for sc in subclasses if sc not in excluded_algorithms) + + # These are deliberately not supported anymore: + if hasattr(hashes, "MD5"): + subclasses.remove(hashes.MD5) + if hasattr(hashes, "SHA1"): + subclasses.remove(hashes.SHA1) + + assert len(constants.HASH_ALGORITHM_NAMES) == len(subclasses) + + # Make sure that keys match the typehint exactly + assert sorted(constants.HASH_ALGORITHM_NAMES.values()) == sorted(typing.get_args(HashAlgorithms)) + + class ReasonFlagsTestCase(TestCase): """Test reason flags.""" @@ -54,44 +102,6 @@ def test_completeness(self) -> None: class CompletenessTestCase(TestCase): """Test for completeness of various constants.""" - def get_subclasses(self, cls: Type[SuperclassTypeVar]) -> Set[Type[SuperclassTypeVar]]: - """Recursively get a list of subclasses. - - .. seealso:: https://stackoverflow.com/a/3862957 - """ - return set(cls.__subclasses__()).union( - [s for c in cls.__subclasses__() for s in self.get_subclasses(c)] - ) - - @property - def supported_hash_algorithms(self) -> typing.Set[Type[hashes.HashAlgorithm]]: - """Get list of supported hash algorithms.""" - subclasses = self.get_subclasses(hashes.HashAlgorithm) # type: ignore[type-var, type-abstract] - - # filter out hash algorithms that are not supported right now due to them having a digest size as - # parameter - subclasses = set( - sc - for sc in subclasses - if sc - not in [ - hashes.SHAKE128, - hashes.SHAKE256, - hashes.BLAKE2b, - hashes.BLAKE2s, - hashes.SM3, - hashes.SHA512_224, - hashes.SHA512_256, - ] - ) - - # These are deliberately not supported anymore: - if hasattr(hashes, "MD5"): - subclasses.remove(hashes.MD5) - if hasattr(hashes, "SHA1"): - subclasses.remove(hashes.SHA1) - return subclasses - def test_elliptic_curves(self) -> None: """Test that ``utils.ELLIPTIC_CURVE_TYPES`` covers all known elliptic curves. @@ -100,7 +110,7 @@ def test_elliptic_curves(self) -> None: """ # MYPY NOTE: mypy does not allow passing abstract classes for type variables, see # https://github.com/python/mypy/issues/5374#issuecomment-436638471 - subclasses = self.get_subclasses(ec.EllipticCurve) # type: ignore[type-var, type-abstract] + subclasses = get_subclasses(ec.EllipticCurve) # type: ignore[type-var, type-abstract] self.assertEqual(len(constants.ELLIPTIC_CURVE_TYPES), len(subclasses)) self.assertEqual(constants.ELLIPTIC_CURVE_TYPES, {e.name: e for e in subclasses}) @@ -122,10 +132,6 @@ def test_extension_keys(self) -> None: """Test completeness of the ``KNOWN_EXTENSION_OIDS`` constant.""" self.assertCountEqual(KNOWN_EXTENSION_OIDS, constants.EXTENSION_KEYS.keys()) - def test_hash_algorithm_names(self) -> None: - """Test completeness of the ``HASH_ALGORITHM_NAMES`` constant.""" - self.assertEqual(len(constants.HASH_ALGORITHM_NAMES), len(self.supported_hash_algorithms)) - def test_nameoid(self) -> None: """Test that we support all NameOID instances.""" known_oids = [v for v in vars(x509.NameOID).values() if isinstance(v, x509.ObjectIdentifier)] diff --git a/ca/django_ca/typehints.py b/ca/django_ca/typehints.py index b4baca915..fe20843f2 100644 --- a/ca/django_ca/typehints.py +++ b/ca/django_ca/typehints.py @@ -175,6 +175,7 @@ def __lt__(self, __other: Any) -> bool: # pragma: nocover # Literals # ############ +#: Valid types of general names. GeneralNames = Literal["email", "URI", "IP", "DNS", "RID", "dirName", "otherName"] #: Valid hash algorithm names. diff --git a/docs/source/conf.py b/docs/source/conf.py index 8202e6746..33b5b54f2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -485,6 +485,7 @@ # `resolve_canonical_names()` below also works, but you still get the same error. ("py:class", "OtherNames"), ("py:class", "KeyUsages"), + ("py:class", "GeneralNames"), # asn1crypto is really used only for OtherNames, so we do not link it ("py:class", "asn1crypto.core.Primitive"), # Pydantic root model signature does not currently work diff --git a/docs/source/django_ca_sphinx/spelling.py b/docs/source/django_ca_sphinx/spelling.py index 019cfee46..61fec2150 100644 --- a/docs/source/django_ca_sphinx/spelling.py +++ b/docs/source/django_ca_sphinx/spelling.py @@ -66,6 +66,8 @@ class MagicWordsFilter(Filter): # type: ignore[misc] "pyOpenSSL", "libffi", "SystemD", + "dirName", + "otherName", } words: typing.ClassVar[typing.Set[str]] = (