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 standard registration form — username and password.
  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',
]

ECP_AUTH = {
    'NONCE_TTL_SECONDS': 300,
    'AUTO_CREATE_USER': True,
}

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
  • asn1crypto >= 1.5

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.0.tar.gz (11.5 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.0-py3-none-any.whl (13.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: django_ecp_signer-0.1.0.tar.gz
  • Upload date:
  • Size: 11.5 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.0.tar.gz
Algorithm Hash digest
SHA256 44ff6a88505d686255936971aa4583d59b9b2d94ea1edc22b09cbe6541896769
MD5 27de7a9bdce59d543f2496b5cf758d29
BLAKE2b-256 2c7e5167299bc3034469b6802e414df0e6eb2d92c8c0cad83c9e8e6c481094bf

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_ecp_signer-0.1.0.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.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_ecp_signer-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7909665ec81ed95e4286f41fcc10e197c3ce205f9b24c172a0f149514460a0db
MD5 7cff48a0c614dda144c3c3212c5767b9
BLAKE2b-256 609285c462577739aa3968e221d737c20d68f6f37bb462af6126783793a6938e

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_ecp_signer-0.1.0-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