django_ca.key_backends - Key backends

django-ca key backends allow you to store private keys in multiple locations. An interface allows you to write custom backends as well.

Supported key backends

class django_ca.key_backends.storages.StoragesBackend(alias: str, storage_alias: str)[source]

The default storage backend that uses Django’s file storage API.

The most common use case for this key backend is to store keys on the local file system. However, you can use any custom Django storage system, for example from django-storages.

This backend takes a single option, storage_alias. It defines the storage system (as defined in STORAGES) to use. The default configuration is a good example:

# The "django-ca" storage alias is mandatory. Added by default in full project
# setups (from source, or with Docker or Docker Compose), but must be set if
# used as Django app. In full project setups, you can also just set CA_DIR to
# change the storage directory.
STORAGES = {
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
    },
    "django-ca": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
        "OPTIONS": {
            "location": "files/",
            "file_permissions_mode": 0o600,
            "directory_permissions_mode": 0o700,
        },
    },
}

CA_KEY_BACKENDS = {
    "default": {
        "BACKEND": "django_ca.key_backends.storages.StoragesBackend",
        "OPTIONS": {"storage_alias": "django-ca"},
    }
}
# The "django-ca" storage alias is mandatory. Added by default in full project
# setups (from source, or with Docker or Docker Compose), but must be set if
# used as Django app. In full project setups, you can also just set CA_DIR to
# change the storage directory.
STORAGES:
  default:
    BACKEND: django.core.files.storage.FileSystemStorage
  staticfiles:
    BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
  django-ca:
    BACKEND: django.core.files.storage.FileSystemStorage
    OPTIONS:
      directory_permissions_mode: 0700
      file_permissions_mode: 0600
      location: files/

CA_KEY_BACKENDS:
  default:
    BACKEND: django_ca.key_backends.storages.StoragesBackend
    OPTIONS:
      storage_alias: django-ca

Writing custom backends

Warning

The key backend interface is new and might change in future versions without notice.

Writing a custom key backend allows you to store keys in a custom way, e.g. in a custom Hardware Security Module (HSM) or using a different library that handles some kind of file storage or dedicated private key storage. Writing such a key backend will require some skills though. You should know at least:

  1. Python

  2. Public key cryptography

  3. Pydantic and Cryptography

Getting started

Implementing a custom key backend requires you to implement a subclass of KeyBackend and up to three Pydantic models to handle options. When implementing, it helps to know that there are essentially three operations, and functions use the appropriate prefix to indicate their purpose:

  1. Create a private key.

  2. Store a private key.

  3. Use the private key for

    1. For creating a self-signed (= root) certificate authority.

    2. For signing an intermediate certificate authority.

    3. For signing certificates (including OCSP responder certificates).

    4. For signing Certificate Revocation Lists (CRLs).

In this tutorial, we’ll write a subset of StoragesBackend. For simplification, we will always create RSA keys with a variable key size.

Options

The three operations described above will typically each require a different set of options. KeyBackend uses Pydantic to load and validate models and pass them around between different functions.

Depending on your needs, options might be stored (or not) in different locations:

  1. In the settings (example: storage_alias in StoragesBackend).

  2. In the database (example: The path where the certificate is stored on the file system).

  3. Given via the command-line (example: The password used to encrypt the private key is not stored).

When implementing KeyBackend, you will be able to handle all three different use cases.

Pydantic Models

First, we will write three Pydantic models to represent the parameters used for creating, storing and using private keys. Please see the extensive Pydantic documentation (in particular on validators) for additional possibilities.

First, let’s represent the options required to create private keys (via manage.py init_ca). This requires all details about the key itself, and also where to store it:

from pathlib import Path

from pydantic import BaseModel


class CreatePrivateKeyOptions(BaseModel):
    """Options for creating private keys."""

    password: bytes | None
    path: Path
    key_size: int

To store private keys (via manage.py import_ca), we don’t need a key size (the key has already been generated), but still a path to store it and an optional password to encrypt it:

from pathlib import Path

from pydantic import BaseModel


class StorePrivateKeyOptions(BaseModel):
    """Options for storing a private key."""

    path: Path
    password: bytes | None

To use the private key (to sign something), we need the password (if one was set), but do not need the path where the key is stored, as it is stored in the database (more on that later):

from pydantic import BaseModel


class UsePrivateKeyOptions(BaseModel):
    """Options for using a private key."""

    password: bytes | None

Note that UsePrivateKeyOptions must be serializable as JSON, so you cannot use arbitrary types here.

Actual implementation

Finally, it is time to implement a class based on KeyBackend.

For a start, you have to define a generic class that uses your three models as type parameters. This will allow you to use mypy for strict type checking later. We’ll also define some properties for that are used by various manage.py commands, as well as options that come from settings and a constructor for some validation:

from django_ca.key_backends import KeyBackend


class MyStoragesBackend(KeyBackend[CreatePrivateKeyOptions, StorePrivateKeyOptions, UsePrivateKeyOptions]):
    """Custom storages implementation."""

    # Used in manage.py commands
    name = "my-storages"
    title = "Store private RSA keys using the Django file storage API"
    description = (
        "It is most commonly used to store private keys on the filesystem. Custom file storage backends can "
        "be used to store keys on other systems (e.g. a cloud storage system)."
    )

    # Used internally:
    use_model = UsePrivateKeyOptions

    # values from the CA_KEY_BACKENDS setting
    storage_alias: str

    def __init__(self, alias: str, storage_alias: str) -> None:
        if storage_alias not in settings.STORAGES:
            raise ValueError(f"{alias}: {storage_alias}: Storage alias is not configured.")
        super().__init__(alias, storage_alias=storage_alias)

Based on the three operations (create, store or use a private key), there are several functions to implement:

  1. add_{operation}_arguments() gets an argparse argument group and allows you to add parameters to the respective manage.py commands.

  2. get_{operation}_options() creates a Pydantic model for that operation based on the arguments added above.

  3. One or more functions that perform an action. They get at least the database model and a model that you created using get_{operation}_options().

Essentially, all you’ll have to do is implement all functions from KeyBackend, and you should have completed the implementation. As an example, let’s demonstrate this for creating private keys, skipping some parameters here:

from pathlib import Path
from typing import TYPE_CHECKING, Any

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

from django_ca.key_backends import KeyBackend
from django_ca.management.actions import PasswordAction
from django_ca.typehints import ArgumentGroup, ParsableKeyType
from pydantic import BaseModel

if TYPE_CHECKING:  # protected by TYPE_CHECKING to avoid circular imports
    from django_ca.models import CertificateAuthority

# {Create,Store,Use}PrivatekeyOptions actually defined above,
# using only shortcuts here:
CreatePrivateKeyOptions = BaseModel
StorePrivateKeyOptions = BaseModel
UsePrivateKeyOptions = BaseModel


class MyStoragesBackend(
    KeyBackend[CreatePrivateKeyOptions, StorePrivateKeyOptions, UsePrivateKeyOptions]
):
    """Custom key backend."""

    # constructor/attributes defined above already
    # def __init__(...): ...

    # Password is needed in multiple places, so create a re-usable function
    def _add_password_argument(
        self,
        group: ArgumentGroup,
        opt: str = "--password",
        prompt: str = "Password for CA: ",
    ) -> None:
        group.add_argument(
            opt,
            nargs="?",
            action=PasswordAction,
            prompt=prompt,
        )

    # Add the arguments for your backend to "manage.py init_ca":
    def add_create_private_key_arguments(self, group: ArgumentGroup) -> None:
        group.add_argument("--key-size", type=int)
        group.add_argument("--path", type=Path, default=Path("ca"))
        self._add_password_argument(group)
        ...

    # If init_ca creates an intermediate CA, it might need a password to load
    # its private key
    def add_use_parent_private_key_arguments(self, group: ArgumentGroup) -> None:
        self._add_password_argument(
            group, opt="--parent-password", prompt="Password for parent CA: "
        )

    # Transform arguments added above into a Pydantic model that contains all
    # information to create a private key. The keys for ``options``
    # correspond to the "destination" of the argparse arguments.
    def get_create_private_key_options(
        self, key_type: ParsableKeyType, options: dict[str, Any]
    ) -> CreatePrivateKeyOptions:
        return CreatePrivateKeyOptions(
            password=options["password"],
            path=options["path"],
            key_size=options["key_size"],
        )

    # Get the model to use the parents private key, if any.
    def get_use_parent_private_key_options(
        self, options: dict[str, Any]
    ) -> UsePrivateKeyOptions:
        return UsePrivateKeyOptions(password=options["parent_password"])

    # Create and store the private key, store database options and return
    # public key and use-model:
    # NOTE: when using the private key, the ``ca.key_backend_options`` and
    # the returned model will be all the information you have to use it.
    def create_private_key(
        self,
        ca: "CertificateAuthority",
        key_type: ParsableKeyType,
        # from get_create_private_key_options():
        options: CreatePrivateKeyOptions,
    ) -> tuple[CertificateIssuerPublicKeyTypes, UsePrivateKeyOptions]:
        # Create the private key and store on filesystem:
        key = ...
        path = ...

        # Store absolute path to private key in database (can contain any
        # JSON-serializable dict):
        ca.key_backend_options = {"path": path}

        # Get model to use the private key later
        use_private_key_options = UsePrivateKeyOptions.model_validate(
            {"password": options.password}, context={"ca": ca}
        )

        return key.public_key(), use_private_key_options

    def sign_certificate(
        self,
        ca: "CertificateAuthority",
        # from create_private_key():
        use_private_key_options: UsePrivateKeyOptions,
        public_key: CertificateIssuerPublicKeyTypes,
        # ...
    ) -> x509.Certificate:
        # load stored private key from ``ca.key_backend_options["path"]``
        # decrypt with ``use_private_key_options.password``
        ...

Base class documentation

class django_ca.key_backends.base.KeyBackend(alias: str, **kwargs: Any)[source]

Base class for all key storage backends.

All implementations of a key storage backend must implement this abstract base class.

abstract add_create_private_key_arguments(group: _ArgumentGroup) None[source]

Add arguments for private key generation with this backend.

Add arguments that can be used for generating private keys with your backend to group. The arguments you add here are expected to be loaded (and validated) using get_create_private_key_options().

add_create_private_key_group(parser: CommandParser) _ArgumentGroup | None[source]

Add an argument group for arguments for private key generation with this backend.

By default, the title and description of the argument group is based on alias, title and description.

Return None if you don’t need to create such a group.

abstract add_store_private_key_arguments(group: _ArgumentGroup) None[source]

Add arguments for storing private keys (when importing an existing CA).

add_store_private_key_group(parser: CommandParser) _ArgumentGroup | None[source]

Add an argument group for storing private keys (when importing an existing CA).

By default, this method adds the same group as add_create_private_key_group()

abstract add_use_parent_private_key_arguments(group: _ArgumentGroup) None[source]

Add arguments for loading the private key of a parent certificate authority.

The arguments you add here are expected to be loaded (and validated) using get_use_parent_private_key_options().

add_use_private_key_arguments(group: _ArgumentGroup) None[source]

Add arguments required for using private key stored with this backend.

The arguments you add here are expected to be loaded (and validated) using get_use_parent_private_key_options().

add_use_private_key_group(parser: CommandParser) _ArgumentGroup | None[source]

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 alias and title.

Return None if you don’t need to create such a group.

alias: str

Alias under which this backend is configured under settings.KEY_BACKENDS.

argparse_prefix: str = ''

Prefix for argparse to use for arguments. Empty for the default alias, and {alias}- otherwise.

abstract check_usable(ca: CertificateAuthority, use_private_key_options: UsePrivateKeyOptionsTypeVar) None[source]

Check if the given CA is usable, raise ValueError if not.

The options are the options returned by get_use_private_key_options(). It may be None in cases where key options cannot (yet) be loaded. If None, the backend should return False if it knows for sure that it will not be usable, and True if usability cannot be determined.

abstract create_private_key(ca: CertificateAuthority, key_type: Literal['RSA', 'DSA', 'EC', 'Ed25519', 'Ed448'], options: CreatePrivateKeyOptionsTypeVar) tuple[DSAPublicKey | RSAPublicKey | EllipticCurvePublicKey | Ed25519PublicKey | Ed448PublicKey, UsePrivateKeyOptionsTypeVar][source]

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 fields are only partially populated.

description: ClassVar[str]

Description used for the ArgumentGroup in manage.py init_ca.

abstract get_create_private_key_options(key_type: Literal['RSA', 'DSA', 'EC', 'Ed25519', 'Ed448'], options: dict[str, Any]) CreatePrivateKeyOptionsTypeVar[source]

Get options to create private keys into a Pydantic model.

options is the dictionary of arguments from manage.py init_ca (including default values). The returned model will be passed to create_private_key().

get_ocsp_key_elliptic_curve(ca: CertificateAuthority, use_private_key_options: UsePrivateKeyOptionsTypeVar) EllipticCurve[source]

Get the default elliptic curve for OCSP keys. This is only called for elliptic curve keys.

get_ocsp_key_size(ca: CertificateAuthority, use_private_key_options: UsePrivateKeyOptionsTypeVar) int[source]

Get the default key size for OCSP keys. This is only called for RSA or DSA keys.

abstract get_store_private_key_options(options: dict[str, Any]) StorePrivateKeyOptionsTypeVar[source]

Get options used when storing private keys.

abstract get_use_parent_private_key_options(ca: CertificateAuthority, options: dict[str, Any]) UsePrivateKeyOptionsTypeVar[source]

Get options to use the private key of a parent certificate authority.

The returned model will be used for the certificate authority ca. You can pass it as extra context to influence model validation.

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.

abstract get_use_private_key_options(ca: CertificateAuthority | None, options: dict[str, Any]) UsePrivateKeyOptionsTypeVar[source]

Get options to use the private key of a certificate authority.

The returned model will be used for the certificate authority ca. You can pass it as extra context to influence model validation. If ca is None, it indicates that the CA is currently being created via manage.py init_ca.

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.

abstract is_usable(ca: CertificateAuthority, use_private_key_options: UsePrivateKeyOptionsTypeVar | None = None) bool[source]

Boolean returning if the given ca can be used to sign new certificates (or CRLs).

The options are the options returned by get_use_private_key_options(). It may be None in cases where key options cannot (yet) be loaded. If None, the backend should return False if it knows for sure that it will not be usable, and True if usability cannot be determined.

options_prefix: str = ''

Prefix to use for loading options. Empty for the default alias, and {alias}_ otherwise.

abstract sign_certificate(ca: CertificateAuthority, use_private_key_options: UsePrivateKeyOptionsTypeVar, public_key: DSAPublicKey | RSAPublicKey | EllipticCurvePublicKey | Ed25519PublicKey | Ed448PublicKey, serial: int, algorithm: SHA224 | SHA256 | SHA384 | SHA512 | SHA3_224 | SHA3_256 | SHA3_384 | SHA3_512 | None, issuer: Name, subject: Name, expires: datetime, extensions: list[Extension[ExtensionType]]) Certificate[source]

Sign a certificate.

abstract sign_certificate_revocation_list(ca: CertificateAuthority, use_private_key_options: UsePrivateKeyOptionsTypeVar, builder: CertificateRevocationListBuilder, algorithm: SHA224 | SHA256 | SHA384 | SHA512 | SHA3_224 | SHA3_256 | SHA3_384 | SHA3_512 | None) CertificateRevocationList[source]

Sign a certificate revocation list request.

abstract store_private_key(ca: CertificateAuthority, key: Ed25519PrivateKey | Ed448PrivateKey | RSAPrivateKey | DSAPrivateKey | EllipticCurvePrivateKey, options: StorePrivateKeyOptionsTypeVar) None[source]

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.

title: ClassVar[str]

Title used for the ArgumentGroup in manage.py init_ca.

use_model: type[UsePrivateKeyOptionsTypeVar]

The Pydantic model representing the options used for loading a private key.