Metadata-Version: 2.4
Name: django-ecp-signer
Version: 0.1.0
Summary: Django authentication via ECDSA digital signatures and PKCS#12 certificates
License: MIT License
        
        Copyright (c) 2026
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
Project-URL: Homepage, https://github.com/zakhkatya/django-ecp-signer
Project-URL: Repository, https://github.com/zakhkatya/django-ecp-signer
Project-URL: Issues, https://github.com/zakhkatya/django-ecp-signer/issues
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: django>=4.2
Requires-Dist: cryptography>=42.0
Dynamic: license-file

# 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.

```bash
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 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 |

> `.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**

```python
INSTALLED_APPS = [
    ...
    'ecp_auth',
]

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

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

**2. urls.py**

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

**3. views.py**

```python
from ecp_auth.mixins import ECPGenerateMixin, ECPLoginMixin

class RegisterView(ECPGenerateMixin, CreateView):
    ...

class LoginView(ECPLoginMixin, DjangoLoginView):
    ...
```

**4. Migrate**

```bash
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

```bash
pip install pytest pytest-django
pytest tests/ -v
```
