Skip to main content

Django authentication via ECDSA digital signatures and PKCS#12 certificates

Project description

django-ecp-auth

Two-factor authentication for Django using digital signatures. Plugs into any existing project via two mixins — no replacement of existing auth logic required.

pip install django-ecp-auth

What is this?

django-ecp-auth adds a digital signature layer on top of standard Django username/password authentication.

Without package With package
username + password username + password + .p12 file

Upon registration, the server generates an ECDSA key pair and issues the user a .p12 file. On every login, the user attaches the file — their browser signs a one-time server challenge with the private key, and the server verifies the signature against the stored public certificate.

The private key never leaves the user's machine after registration. The server stores only the public certificate.


Key concepts

Private key — lives only in the user's .p12 file. Used to sign the challenge. Never sent to the server.

Public key — embedded in the X.509 certificate stored in the database. Used to verify the signature.

Nonce — a random string generated by the server for each login attempt. The client signs exactly this string. This prevents replay attacks: an intercepted signature is useless because it was made for a specific one-time value that is immediately marked as used.


Registration

  1. User submits the registration form — username, password, and taxpayer_id (10-digit Ukrainian tax ID / ІПН). The taxpayer_id field must be present in form.cleaned_data — add it to your registration form.
  2. The existing RegisterView creates the User object as usual.
  3. ECPGenerateMixin runs automatically after form_valid():
    • Generates an ECDSA P-256 key pair.
    • Builds an X.509 certificate with username as the identity field.
    • Saves only the public certificate to the database (ECPCertificate).
    • Serializes the private key + certificate into a .p12 file (no password).
    • Stores the .p12 bytes in the session. The private key is then discarded.
  4. User clicks "Download your key" — user.p12 is served as a file attachment and removed from the session.

After registration the server retains no private key — it exists only on the user's disk.


Login

  1. The login page automatically fetches a nonce from GET /ecp/challenge/.
  2. User fills in username, password, and attaches their .p12 file.
  3. JavaScript reads the file, extracts the private key, and signs the nonce.
  4. The form submits username, password, nonce_id, signature, and taxpayer_id.
  5. The server runs two checks in sequence:
    • Password check — standard Django authenticate(). Fails fast if credentials are wrong.
    • Signature check — fetches the certificate from the database (never from the client), verifies the ECDSA signature against the nonce, and checks that the certificate has not expired.
  6. On success: nonce is consumed (used=True), session is opened, user is redirected to /dashboard/.

What is stored where

User's diskuser.p12 containing the private key and certificate. Without this file, login is impossible.

Database — hashed password in users_user, public certificate in ecp_auth_ecpcertificate, one-time nonces in ecp_auth_ecpnonce. No private key is ever written to the database.


Security

Threat Defense
Replay attack Nonce is marked used=True after first use
Forged signature ECDSA verification fails
Key mismatch (wrong .p12) Server uses certificate from DB, not from client
Expired certificate is_expired() check on every login
Stale nonce Nonces expire after 300 seconds
Password stolen, no .p12 Cannot produce a valid signature without the private key
.p12 stolen, no password authenticate() rejects wrong credentials before signature is checked

.p12 files are not password-protected by default. For production deployments, encrypting the file with a separate passphrase is strongly recommended.


Setup

1. settings.py

INSTALLED_APPS = [
    ...
    'ecp_auth',
]

AUTHENTICATION_BACKENDS = [
    'ecp_auth.backends.ECPAuthenticationBackend',
]

# Optional: nonce lifetime (default 5 minutes)
from datetime import timedelta
NONCE_LIFETIME = timedelta(minutes=5)

2. urls.py

path("ecp/", include("ecp_auth.urls")),

3. views.py

from ecp_auth.mixins import ECPGenerateMixin, ECPLoginMixin

class RegisterView(ECPGenerateMixin, CreateView):
    ...

class LoginView(ECPLoginMixin, DjangoLoginView):
    ...

4. Migrate

python manage.py migrate

Endpoints

Method URL Description
GET /ecp/challenge/ Returns a one-time nonce
GET /ecp/certificate/download/ Downloads the generated user.p12

Exceptions

All exceptions inherit from ECPAuthError so you can catch them broadly or specifically.

ECPAuthError
├── InvalidSignatureError
├── CertificateExpiredError
├── InvalidCertificateError
├── NonceExpiredError
└── NonceNotFoundError

Requirements

  • Python >= 3.12
  • Django >= 4.2
  • cryptography >= 42.0

Running tests

pip install pytest pytest-django
pytest tests/ -v

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_ecp_signer-0.1.1.tar.gz (12.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_ecp_signer-0.1.1-py3-none-any.whl (13.9 kB view details)

Uploaded Python 3

File details

Details for the file django_ecp_signer-0.1.1.tar.gz.

File metadata

  • Download URL: django_ecp_signer-0.1.1.tar.gz
  • Upload date:
  • Size: 12.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_ecp_signer-0.1.1.tar.gz
Algorithm Hash digest
SHA256 c74014ec3e0fa7bcaf42274d36b81c434bcc95e47944709f24397bd83084e48d
MD5 2d56a692d0a142ed1abe64166bd2be77
BLAKE2b-256 ef89c9acd649347b377ece27ac97b41aa5e1d280ae87bb18b0c3fc7db8aa512c

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_ecp_signer-0.1.1.tar.gz:

Publisher: publish.yml on zakhkatya/django-ecp-signer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_ecp_signer-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_ecp_signer-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 279590677e083263626cd9644e4b85dd61b451a744abd7c1b0b592cab8fdfd71
MD5 6b171420052d8a1462309148906d4708
BLAKE2b-256 30abc536f2ed7e95d3455d5ca5c475f2bdeaab880fc40ab447e58de92574d5ac

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_ecp_signer-0.1.1-py3-none-any.whl:

Publisher: publish.yml on zakhkatya/django-ecp-signer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page