Skip to content

Commit

Permalink
fix more test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
mathiasertl committed Mar 3, 2024
1 parent ad49337 commit 56e50c9
Show file tree
Hide file tree
Showing 41 changed files with 771 additions and 777 deletions.
44 changes: 16 additions & 28 deletions ca/django_ca/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,9 +659,6 @@ def get_ca_details(self) -> Dict[int, Dict[str, Any]]:
"""Get CA details for the embedded JSON data."""
data: Dict[int, Dict[str, Any]] = {}
for ca in CertificateAuthority.objects.usable():
if ca.key_exists is False:
continue

extensions = SignCertificateExtensionsList.validate_python(ca.extensions_for_certificate.values())

hash_algorithm_name: Optional[str] = None
Expand All @@ -677,10 +674,7 @@ def get_ca_details(self) -> Dict[int, Dict[str, Any]]:

def has_add_permission(self, request: HttpRequest) -> bool:
# Only grant add permissions if there is at least one usable CA
for ca in CertificateAuthority.objects.usable():
if ca.key_exists:
return True
return False
return CertificateAuthority.objects.usable().exists()

def csr_pem(self, obj: Certificate) -> str:
"""Get the CSR in PEM form for display."""
Expand Down Expand Up @@ -743,21 +737,15 @@ def get_changeform_initial_data(self, request: HttpRequest) -> Dict[str, Any]:
else:
# Form for a completely new certificate

ca = None
try:
ca = CertificateAuthority.objects.default()
except ImproperlyConfigured as ex:
except ImproperlyConfigured as ex: # pragma: no cover
# NOTE: This should not happen because if no CA is usable from the admin interface, the "add"
# button would not even show up.
log.error(ex)
ca = CertificateAuthority.objects.usable().order_by("-expires", "serial").first()

# If the default CA is not usable, use the first one that we can use instead.
if ca is None or ca.key_exists is False:
for usable_ca in CertificateAuthority.objects.usable().order_by("-expires", "serial"):
if usable_ca.key_exists:
ca = usable_ca

# NOTE: This should not happen because if no CA is usable from the admin interface, the "add"
# button would not even show up.
if ca is None: # pragma: no cover
if ca is None:
raise ImproperlyConfigured("Cannot determine default CA.")

profile = profiles[ca_settings.CA_DEFAULT_PROFILE]
Expand Down Expand Up @@ -1051,18 +1039,18 @@ def save_model( # type: ignore[override]
extensions[oid] = ext # pragma: no cover # all extensions should be handled above!

ca: CertificateAuthority = data["ca"]
certificate = ca.sign(
data["key_backend_options"],
csr,
subject=data["subject"],
algorithm=data["algorithm"],
expires=expires,
extensions=extensions.values(),
password=data["password"],
)

obj.profile = profile.name
obj.update_certificate(
ca.sign(
csr,
subject=data["subject"],
algorithm=data["algorithm"],
expires=expires,
extensions=extensions.values(),
password=data["password"],
)
)
obj.update_certificate(certificate)
obj.save()
post_issue_cert.send(sender=self.model, cert=obj)
else:
Expand Down
3 changes: 2 additions & 1 deletion ca/django_ca/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ class Meta(X509BaseSchema.Meta): # pylint: disable=missing-class-docstring
@staticmethod
def resolve_can_sign_certificates(obj: CertificateAuthority) -> bool:
"""Resolve the can_sign_certificates flag."""
return obj.key_exists
# TODO: re-implement using backends
return True


class CertificateAuthorityFilterSchema(Schema):
Expand Down
96 changes: 80 additions & 16 deletions ca/django_ca/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@

from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.types import CertificateIssuerPublicKeyTypes
from cryptography.hazmat.primitives.asymmetric.types import (
CertificateIssuerPublicKeyTypes,
CertificateIssuerPrivateKeyTypes,
)

from django.core.exceptions import ImproperlyConfigured
from django.core.management import CommandParser
Expand All @@ -36,11 +39,17 @@


Self = typing.TypeVar("Self", bound="KeyBackend") # pragma: only py<3.11 # replace with typing.Self
CreatePrivateKeyOptions = typing.TypeVar("CreatePrivateKeyOptions", bound=BaseModel)
LoadPrivateKeyOptions = typing.TypeVar("LoadPrivateKeyOptions", bound=BaseModel)
CreatePrivateKeyOptionsTypeVar = typing.TypeVar("CreatePrivateKeyOptionsTypeVar", bound=BaseModel)
UsePrivateKeyOptionsTypeVar = typing.TypeVar("UsePrivateKeyOptionsTypeVar", bound=BaseModel)
StorePrivateKeyOptionsTypeVar = typing.TypeVar("StorePrivateKeyOptionsTypeVar", bound=BaseModel)


class KeyBackend(typing.Generic[CreatePrivateKeyOptions, LoadPrivateKeyOptions], metaclass=abc.ABCMeta):
class KeyBackend(
typing.Generic[
CreatePrivateKeyOptionsTypeVar, StorePrivateKeyOptionsTypeVar, UsePrivateKeyOptionsTypeVar
],
metaclass=abc.ABCMeta,
):
"""Base class for all key storage backends.
All implementations of a key storage backend must implement this abstract base class.
Expand All @@ -56,7 +65,7 @@ class KeyBackend(typing.Generic[CreatePrivateKeyOptions, LoadPrivateKeyOptions],
description: typing.ClassVar[str]

#: The Pydantic model representing the options used for loading a private key.
load_model: Type[LoadPrivateKeyOptions]
load_model: Type[UsePrivateKeyOptionsTypeVar]

def __init__(self, alias: str, **kwargs: Any) -> None:
self.alias = alias
Expand All @@ -83,6 +92,28 @@ def add_private_key_group(self, parser: CommandParser) -> Optional[ArgumentGroup
f"The backend used with --key-backend={self.alias}. {self.description}",
)

def add_store_private_key_group(self, parser: CommandParser) -> Optional[ArgumentGroup]:
"""Add an argument group for storing private keys (when importing an existing CA).
By default, this method adds the same group as
:py:func:`~django_ca.backends.base.KeyBackend.add_private_key_group`
"""
return self.add_private_key_group(parser)

def add_use_private_key_group(self, parser: CommandParser) -> Optional[ArgumentGroup]:
"""Add an argument group for arguments required for using a private key stored with this backend.
By default, the title and description of the argument group is based on
:py:attr:`~django_ca.backends.base.KeyBackend.alias` and
:py:attr:`~django_ca.backends.base.KeyBackend.title`.
Return ``None`` if you don't need to create such a group.
"""
return parser.add_argument_group(
f"{self.alias} key storage",
f"Arguments for using private keys stored with the {self.alias} backend.",
)

def add_private_key_arguments(self, group: ArgumentGroup) -> None: # pylint: disable=unused-argument
"""Add arguments for private key generation with this backend.
Expand All @@ -101,55 +132,86 @@ def add_parent_private_key_arguments(self, group: ArgumentGroup) -> None:
"""
return None

def add_use_private_key_arguments(self, group: ArgumentGroup) -> None:
"""Add arguments required for using private key stored with this backend.
The arguments you add here are expected to be loaded (and validated) using
:py:func:`~django_ca.backends.base.KeyBackend.get_load_parent_private_key_options`.
"""
return None

@abc.abstractmethod
def get_create_private_key_options(
self, key_type: ParsableKeyType, options: Dict[str, Any]
) -> CreatePrivateKeyOptions:
) -> CreatePrivateKeyOptionsTypeVar:
"""Load options to create private keys into a Pydantic model.
`options` is the dictionary of arguments to ``manage.py init_ca`` (including default values). The
returned model will be passed to :py:func:`~django_ca.backends.base.KeyBackend.create_private_key`.
"""

@abc.abstractmethod
def get_load_private_key_options(self, options: Dict[str, Any]) -> LoadPrivateKeyOptions:
def add_store_private_key_options(self, options: Dict[str, Any]) -> StorePrivateKeyOptionsTypeVar:
"""Add arguments for storing private keys (when importing an existing CA)."""

@abc.abstractmethod
def get_load_private_key_options(self, options: Dict[str, Any]) -> UsePrivateKeyOptionsTypeVar:
"""Load options to create private keys into a Pydantic model.
`options` is the dictionary of arguments to ``manage.py init_ca`` (including default values). The key
backend is expected to be able to sign certificates and CRLs using the options provided here.
"""

@abc.abstractmethod
def get_load_parent_private_key_options(self, options: Dict[str, Any]) -> LoadPrivateKeyOptions:
def get_store_private_key_options(self, options: Dict[str, Any]) -> StorePrivateKeyOptionsTypeVar:
...

@abc.abstractmethod
def get_load_parent_private_key_options(self, options: Dict[str, Any]) -> UsePrivateKeyOptionsTypeVar:
"""Load options to create private keys into a Pydantic model.
`options` is the dictionary of arguments to ``manage.py init_ca`` (including default values). The key
backend is expected to be able to sign certificate authorities using the options provided here.
"""

@abc.abstractmethod
def is_usable(self, ca: "CertificateAuthority", options: LoadPrivateKeyOptions) -> bool:
def is_usable(self, ca: "CertificateAuthority", options: UsePrivateKeyOptionsTypeVar) -> bool:
...

@abc.abstractmethod
def create_private_key(
self, ca: "CertificateAuthority", key_type: ParsableKeyType, options: CreatePrivateKeyOptions
self, ca: "CertificateAuthority", key_type: ParsableKeyType, options: CreatePrivateKeyOptionsTypeVar
) -> CertificateIssuerPublicKeyTypes:
"""Create a private key for the certificate authority.
The method is expected to set `key_backend_options` on `ca` with a set of options that can later be
used to load the private key. Since this value will be stored in the database, you should not add
secrets to `key_backend_options`.
Note that `ca` is not yet a *saved* database entity, so it does not have a private key and fields
populated will be incomplete.
Note that `ca` is not yet a *saved* database entity, so fields are only partially populated.
"""

@abc.abstractmethod
def store_private_key(
self,
ca: "CertificateAuthority",
key: CertificateIssuerPrivateKeyTypes,
options: StorePrivateKeyOptionsTypeVar,
) -> None:
"""Store a private key for the certificate authority.
The method is expected to set `key_backend_options` on `ca` with a set of options that can later be
used to load the private key. Since this value will be stored in the database, you should not add
secrets to `key_backend_options`.
Note that `ca` is not yet a *saved* database entity, so fields are only partially populated.
"""

@abc.abstractmethod
def sign_certificate(
self,
ca: "CertificateAuthority",
load_options: LoadPrivateKeyOptions,
load_options: UsePrivateKeyOptionsTypeVar,
public_key: CertificateIssuerPublicKeyTypes,
serial: int,
algorithm: Optional[AllowedHashTypes],
Expand All @@ -164,17 +226,19 @@ def sign_certificate(
def sign_certificate_revocation_list(
self,
ca: "CertificateAuthority",
load_options: LoadPrivateKeyOptions,
load_options: UsePrivateKeyOptionsTypeVar,
builder: x509.CertificateRevocationListBuilder,
algorithm: Optional[AllowedHashTypes],
) -> x509.CertificateRevocationList:
"""Sign a certificate revocation list request."""

def get_ocsp_key_size(self) -> int:
def get_ocsp_key_size(self, ca: "CertificateAuthority", load_options: UsePrivateKeyOptionsTypeVar) -> int:
"""Get the default key size for OCSP keys. This is only called for RSA or DSA keys."""
return ca_settings.CA_DEFAULT_KEY_SIZE

def get_ocsp_key_elliptic_curve(self) -> ec.EllipticCurve:
def get_ocsp_key_elliptic_curve(
self, ca: "CertificateAuthority", load_options: UsePrivateKeyOptionsTypeVar
) -> ec.EllipticCurve:
"""Get the default elliptic curve for OCSP keys. This is only called for elliptic curve keys."""
return ca_settings.CA_DEFAULT_ELLIPTIC_CURVE()

Expand Down
Loading

0 comments on commit 56e50c9

Please sign in to comment.