Skip to content

Commit

Permalink
store certificate revocation lists in database
Browse files Browse the repository at this point in the history
  • Loading branch information
mathiasertl committed Oct 15, 2024
1 parent 88b175b commit 0f9ab14
Show file tree
Hide file tree
Showing 40 changed files with 2,028 additions and 1,079 deletions.
104 changes: 85 additions & 19 deletions ca/django_ca/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@
from importlib.util import find_spec
from typing import Annotated, Any, Literal, Optional, Union, cast

from annotated_types import Ge, Le, MinLen
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, field_validator, model_validator
from annotated_types import Ge, Le
from pydantic import (
BaseModel,
BeforeValidator,
ConfigDict,
Field,
field_validator,
model_validator,
)

from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509.oid import NameOID

from django.conf import settings as _settings
Expand All @@ -35,26 +41,26 @@
from django.utils.translation import gettext_lazy as _

from django_ca import constants
from django_ca.deprecation import RemovedInDjangoCA220Warning
from django_ca.deprecation import RemovedInDjangoCA220Warning, RemovedInDjangoCA230Warning
from django_ca.pydantic import NameModel
from django_ca.pydantic.type_aliases import (
CertificateRevocationListEncodingTypeAlias,
CertificateRevocationListReasonCode,
EllipticCurveTypeAlias,
HashAlgorithmTypeAlias,
PowerOfTwoInt,
Serial,
UniqueElementsTuple,
)
from django_ca.pydantic.validators import name_oid_parser, timedelta_as_number_parser
from django_ca.pydantic.validators import crl_scope_validator, name_oid_parser, timedelta_as_number_parser
from django_ca.typehints import (
AllowedHashTypes,
CertificateRevocationListScopes,
ConfigurableExtension,
ConfigurableExtensionKeys,
ParsableKeyType,
Self,
)

CRLEncodings = Annotated[frozenset[CertificateRevocationListEncodingTypeAlias], MinLen(1)]
# BeforeValidator currently does not work together with Le(), see:
# https://github.com/pydantic/pydantic/issues/10459
# TimedeltaAsDays = Annotated[timedelta, BeforeValidator(timedelta_as_number_parser("days"))]
Expand Down Expand Up @@ -244,23 +250,87 @@ def _subject_validator(value: Any) -> Any:
Subject = Annotated[x509.Name, BeforeValidator(_subject_validator)]


class CertificateRevocationListProfileOverride(BaseModel):
class CertificateRevociationListBaseModel(BaseModel):
"""Base model for CRL profiles and overrides."""

encodings: Optional[Any] = None
scope: Optional[CertificateRevocationListScopes] = None
only_contains_ca_certs: bool = False
only_contains_user_certs: bool = False
only_contains_attribute_certs: bool = False
only_some_reasons: Optional[frozenset[CertificateRevocationListReasonCode]] = None

@field_validator("encodings")
@classmethod
def warn_encodings(cls, v: Any) -> Any:
"""Validator to warn that encodings is now unused."""
warnings.warn(
"encodings: Setting has no effect starting with django-ca 2.1.0.",
RemovedInDjangoCA230Warning,
stacklevel=1,
)
return v

@model_validator(mode="after")
def validate_scope(self) -> Self:
"""Validate the scope of the CRL."""
if "scope" in self.model_fields_set:
warnings.warn(
"scope: Setting is deprecated and will be removed in django-ca 2.3.0. Use "
"`only_contains_ca_certs` and `only_contains_user_certs` instead.",
RemovedInDjangoCA230Warning,
stacklevel=1,
)
if self.scope == "user":
self.only_contains_user_certs = True
if self.scope == "ca":
self.only_contains_ca_certs = True
if self.scope == "attribute":
self.only_contains_attribute_certs = True
self.scope = None

crl_scope_validator(
self.only_contains_ca_certs,
self.only_contains_user_certs,
self.only_contains_attribute_certs,
None, # already validated by type alias for field
)
return self


class CertificateRevocationListProfileOverride(CertificateRevociationListBaseModel):
"""Model for overriding fields of a CRL Profile."""

encodings: Optional[CRLEncodings] = None
expires: Optional[timedelta] = None
scope: Optional[CertificateRevocationListScopes] = None
skip: bool = False


class CertificateRevocationListProfile(BaseModel):
class CertificateRevocationListProfile(CertificateRevociationListBaseModel):
"""Model for profiles for CRL generation."""

encodings: CRLEncodings
expires: timedelta = timedelta(days=1)
scope: Optional[CertificateRevocationListScopes] = None
OVERRIDES: dict[Serial, CertificateRevocationListProfileOverride] = Field(default_factory=dict)

@model_validator(mode="after")
def validate_overrides(self) -> Self:
"""Validate that overrides do not create an invalid scope."""
# pylint: disable-next=no-member # pylint doesn't recognize the type of OVERRIDES.
for override in self.OVERRIDES.values():
only_contains_user_certs = self.only_contains_user_certs
only_contains_ca_certs = self.only_contains_ca_certs
only_contains_attribute_certs = self.only_contains_attribute_certs
if "only_contains_ca_certs" in override.model_fields_set:
only_contains_ca_certs = override.only_contains_ca_certs
if "only_contains_user_certs" in override.model_fields_set:
only_contains_user_certs = override.only_contains_user_certs
if "only_contains_attribute_certs" in override.model_fields_set:
only_contains_attribute_certs = override.only_contains_attribute_certs

crl_scope_validator( # only_some_reasons is already validated by type alias for field
only_contains_ca_certs, only_contains_user_certs, only_contains_attribute_certs, None
)
return self


class KeyBackendConfigurationModel(BaseModel):
"""Base model for a key backend configuration."""
Expand Down Expand Up @@ -300,12 +370,8 @@ class SettingsModel(BaseModel):
CA_ACME_MAX_CERT_VALIDITY: AcmeCertValidity = timedelta(days=90)

CA_CRL_PROFILES: dict[str, CertificateRevocationListProfile] = {
"user": CertificateRevocationListProfile(
expires=timedelta(days=1), scope="user", encodings=[Encoding.PEM, Encoding.DER]
),
"ca": CertificateRevocationListProfile(
expires=timedelta(days=1), scope="ca", encodings=[Encoding.PEM, Encoding.DER]
),
"user": CertificateRevocationListProfile(only_contains_user_certs=True),
"ca": CertificateRevocationListProfile(only_contains_ca_certs=True),
}
CA_DEFAULT_CA: Optional[Serial] = None
CA_DEFAULT_DSA_SIGNATURE_HASH_ALGORITHM: HashAlgorithmTypeAlias = hashes.SHA256()
Expand Down
20 changes: 19 additions & 1 deletion ca/django_ca/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
CertificateRevocationListEncodingNames,
CertificateRevocationListEncodings,
ConfigurableExtensionKeys,
DistributionPointReasons,
EllipticCurves,
EndEntityCertificateExtensionKeys,
ExtensionKeys,
Expand All @@ -71,6 +72,18 @@
CertificateRevocationListEncodings, CertificateRevocationListEncodingNames
] = MappingProxyType({v: k for k, v in CERTIFICATE_REVOCATION_LIST_ENCODING_TYPES.items()})

#: Reasons that are valid in `only_some_reasons` of a CRL.
CERTIFICATE_REVOCATION_LIST_REASONS = [
reason
for reason in x509.ReasonFlags
if reason not in (x509.ReasonFlags.unspecified, x509.ReasonFlags.remove_from_crl)
]

#: Names of the reasons that are valid in `only_some_reasons` of a CRL.
CERTIFICATE_REVOCATION_LIST_REASON_NAMES: DistributionPointReasons = [ # type: ignore[assignment]
reason.name for reason in CERTIFICATE_REVOCATION_LIST_REASONS
]

DEFAULT_STORAGE_BACKEND = "django_ca.key_backends.storages.StoragesBackend"

#: Mapping of elliptic curve names to the implementing classes
Expand Down Expand Up @@ -637,7 +650,7 @@ class ReasonFlags(enum.Enum):
remove_from_crl = "removeFromCRL"


#: Mapping of RFC 5280, section 5.3.1 reason codes too cryptography reason codes
#: Mapping of RFC 5280, section 5.3.1 reason codes to cryptography reason codes
REASON_CODES = {
0: ReasonFlags.unspecified,
1: ReasonFlags.key_compromise,
Expand All @@ -664,3 +677,8 @@ class ReasonFlags(enum.Enum):
(ReasonFlags.superseded.name, _("Superseded")),
(ReasonFlags.unspecified.name, _("Unspecified")),
)

CRL_SCOPE_ERROR = (
"Only one of `only_contains_ca_certs`, `only_contains_user_certs` and "
"`only_contains_attribute_certs` can be set."
)
12 changes: 8 additions & 4 deletions ca/django_ca/management/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import getpass
import typing
from datetime import timedelta
from typing import Any, Optional
from typing import Any, Literal, Optional

from pydantic import BaseModel

Expand Down Expand Up @@ -162,17 +162,21 @@ class ExpiresAction(SingleValueAction[str, timedelta]):
3
"""

def __init__(self, unit: Literal["days", "hours", "minutes", "seconds"] = "days", **kwargs: Any) -> None:
super().__init__(**kwargs)
self.unit = unit

def parse_value(self, value: str) -> timedelta:
"""Parse the value for this action."""
# NOTE: Making this a member of ExpiresAction causes an infinite loop for some reason
try:
days = int(value)
parsed = int(value)
except ValueError as ex:
raise argparse.ArgumentError(self, f"{value}: Value must be an integer.") from ex
if days <= 0:
if parsed <= 0:
raise argparse.ArgumentError(self, f"{value}: Value must not be negative.")

return timedelta(days=days)
return timedelta(**{self.unit: parsed}) # type: ignore[misc] # mypy does not expect Literal as str


class FormatAction(SingleValueAction[str, Encoding]):
Expand Down
Loading

0 comments on commit 0f9ab14

Please sign in to comment.