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
- User submits the standard registration form —
usernameandpassword. - The existing
RegisterViewcreates theUserobject as usual. ECPGenerateMixinruns automatically afterform_valid():- Generates an ECDSA P-256 key pair.
- Builds an X.509 certificate with
usernameas the identity field. - Saves only the public certificate to the database (
ECPCertificate). - Serializes the private key + certificate into a
.p12file (no password). - Stores the
.p12bytes in the session. The private key is then discarded.
- User clicks "Download your key" —
user.p12is 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
- The login page automatically fetches a nonce from
GET /ecp/challenge/. - User fills in
username,password, and attaches their.p12file. - JavaScript reads the file, extracts the private key, and signs the nonce.
- The form submits
username,password,nonce_id,signature, andtaxpayer_id. - 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.
- Password check — standard Django
- On success: nonce is consumed (
used=True), session is opened, user is redirected to/dashboard/.
What is stored where
User's disk — user.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 |
.p12files 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
44ff6a88505d686255936971aa4583d59b9b2d94ea1edc22b09cbe6541896769
|
|
| MD5 |
27de7a9bdce59d543f2496b5cf758d29
|
|
| BLAKE2b-256 |
2c7e5167299bc3034469b6802e414df0e6eb2d92c8c0cad83c9e8e6c481094bf
|
Provenance
The following attestation bundles were made for django_ecp_signer-0.1.0.tar.gz:
Publisher:
publish.yml on zakhkatya/django-ecp-signer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_ecp_signer-0.1.0.tar.gz -
Subject digest:
44ff6a88505d686255936971aa4583d59b9b2d94ea1edc22b09cbe6541896769 - Sigstore transparency entry: 1186438612
- Sigstore integration time:
-
Permalink:
zakhkatya/django-ecp-signer@92575ad3e3426b5f82961432d392de9b60922b94 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/zakhkatya
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@92575ad3e3426b5f82961432d392de9b60922b94 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_ecp_signer-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_ecp_signer-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7909665ec81ed95e4286f41fcc10e197c3ce205f9b24c172a0f149514460a0db
|
|
| MD5 |
7cff48a0c614dda144c3c3212c5767b9
|
|
| BLAKE2b-256 |
609285c462577739aa3968e221d737c20d68f6f37bb462af6126783793a6938e
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_ecp_signer-0.1.0-py3-none-any.whl -
Subject digest:
7909665ec81ed95e4286f41fcc10e197c3ce205f9b24c172a0f149514460a0db - Sigstore transparency entry: 1186438613
- Sigstore integration time:
-
Permalink:
zakhkatya/django-ecp-signer@92575ad3e3426b5f82961432d392de9b60922b94 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/zakhkatya
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@92575ad3e3426b5f82961432d392de9b60922b94 -
Trigger Event:
push
-
Statement type: