Skip to content

Commit

Permalink
test general names for completeness
Browse files Browse the repository at this point in the history
  • Loading branch information
mathiasertl committed Dec 28, 2023
1 parent 45cb3e2 commit edaf092
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 49 deletions.
14 changes: 10 additions & 4 deletions ca/django_ca/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down Expand Up @@ -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,
Expand All @@ -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()}
)

Expand Down
96 changes: 51 additions & 45 deletions ca/django_ca/tests/test_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -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."""

Expand All @@ -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.
Expand All @@ -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})

Expand All @@ -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)]
Expand Down
1 change: 1 addition & 0 deletions ca/django_ca/typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/source/django_ca_sphinx/spelling.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class MagicWordsFilter(Filter): # type: ignore[misc]
"pyOpenSSL",
"libffi",
"SystemD",
"dirName",
"otherName",
}

words: typing.ClassVar[typing.Set[str]] = (
Expand Down

0 comments on commit edaf092

Please sign in to comment.