ecp-lib 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ecp_lib-0.1.0/LICENSE +21 -0
- ecp_lib-0.1.0/MANIFEST.in +3 -0
- ecp_lib-0.1.0/PKG-INFO +334 -0
- ecp_lib-0.1.0/README.md +303 -0
- ecp_lib-0.1.0/ecp_lib/__init__.py +46 -0
- ecp_lib-0.1.0/ecp_lib/apps.py +6 -0
- ecp_lib-0.1.0/ecp_lib/auth.py +107 -0
- ecp_lib-0.1.0/ecp_lib/crypto.py +119 -0
- ecp_lib-0.1.0/ecp_lib/middleware.py +164 -0
- ecp_lib-0.1.0/ecp_lib/migrations/0001_initial.py +36 -0
- ecp_lib-0.1.0/ecp_lib/migrations/__init__.py +1 -0
- ecp_lib-0.1.0/ecp_lib/models.py +21 -0
- ecp_lib-0.1.0/ecp_lib/validators.py +60 -0
- ecp_lib-0.1.0/ecp_lib.egg-info/PKG-INFO +334 -0
- ecp_lib-0.1.0/ecp_lib.egg-info/SOURCES.txt +20 -0
- ecp_lib-0.1.0/ecp_lib.egg-info/dependency_links.txt +1 -0
- ecp_lib-0.1.0/ecp_lib.egg-info/requires.txt +4 -0
- ecp_lib-0.1.0/ecp_lib.egg-info/top_level.txt +1 -0
- ecp_lib-0.1.0/pyproject.toml +47 -0
- ecp_lib-0.1.0/setup.cfg +4 -0
- ecp_lib-0.1.0/setup.py +4 -0
- ecp_lib-0.1.0/tests/test_ecp_lib.py +166 -0
ecp_lib-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 toksik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ecp_lib-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ecp-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Django RSA private-key authentication helpers and middleware
|
|
5
|
+
Author: toksik
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/toksik/django-pub-sub
|
|
8
|
+
Project-URL: Repository, https://github.com/toksik/django-pub-sub
|
|
9
|
+
Project-URL: Issues, https://github.com/toksik/django-pub-sub/issues
|
|
10
|
+
Keywords: django,rsa,authentication,middleware,pem,cryptography
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: Django :: 4.2
|
|
14
|
+
Classifier: Framework :: Django :: 5.0
|
|
15
|
+
Classifier: Framework :: Django :: 5.1
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Security :: Cryptography
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: cryptography>=46.0.6
|
|
28
|
+
Provides-Extra: django
|
|
29
|
+
Requires-Dist: django<6.0,>=4.2; extra == "django"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# ecp-lib
|
|
33
|
+
|
|
34
|
+
`ecp-lib` — Django-бібліотека для входу з `username + password + private.pem`.
|
|
35
|
+
Вона генерує RSA-ключі, зберігає `public_key` користувача і дає middleware, яке перевіряє, що завантажений приватний ключ відповідає ключу в БД.
|
|
36
|
+
|
|
37
|
+
## Що вміє бібліотека
|
|
38
|
+
|
|
39
|
+
- генерувати пару `private_key/public_key`
|
|
40
|
+
- зберігати `public_key` у моделі `ECPKey`
|
|
41
|
+
- читати `private.pem` з upload
|
|
42
|
+
- перевіряти пару `username/password/private_key`
|
|
43
|
+
- відсікати невалідні логін-запити ще в middleware
|
|
44
|
+
- логувати, чи запит взагалі дійшов до middleware і на якому кроці впав
|
|
45
|
+
|
|
46
|
+
## Що бібліотека не робить
|
|
47
|
+
|
|
48
|
+
- не дає готових `views` або `urls`
|
|
49
|
+
- не логінить користувача сама по собі
|
|
50
|
+
- не зберігає `private.pem` на сервері
|
|
51
|
+
- не будує challenge-response протокол
|
|
52
|
+
|
|
53
|
+
## Встановлення
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install ecp-lib
|
|
57
|
+
pip install "ecp-lib[django]"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Публічне API
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from ecp_lib import (
|
|
64
|
+
ECPKey,
|
|
65
|
+
ECPMiddleware,
|
|
66
|
+
authenticate_with_private_key,
|
|
67
|
+
create_challenge,
|
|
68
|
+
create_user_keys,
|
|
69
|
+
generate_keys,
|
|
70
|
+
read_private_key,
|
|
71
|
+
sanitize,
|
|
72
|
+
sign,
|
|
73
|
+
validate_public_key,
|
|
74
|
+
verify,
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Основний Django-flow використовує:
|
|
79
|
+
|
|
80
|
+
- `create_user_keys()`
|
|
81
|
+
- `read_private_key()`
|
|
82
|
+
- `authenticate_with_private_key()`
|
|
83
|
+
- `ECPMiddleware`
|
|
84
|
+
|
|
85
|
+
## Основний flow
|
|
86
|
+
|
|
87
|
+
### 1. Реєстрація
|
|
88
|
+
|
|
89
|
+
Після створення користувача виклич `create_user_keys(user)`.
|
|
90
|
+
Функція:
|
|
91
|
+
|
|
92
|
+
1. генерує нову RSA-пару
|
|
93
|
+
2. зберігає `public_key` у `ECPKey`
|
|
94
|
+
3. повертає `private_key` як PEM-рядок
|
|
95
|
+
|
|
96
|
+
Типовий варіант: віддати цей PEM користувачу як файл `private.pem`.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from django.http import HttpResponse
|
|
100
|
+
|
|
101
|
+
from ecp_lib.auth import create_user_keys
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def register_success_response(user):
|
|
105
|
+
private_key = create_user_keys(user)
|
|
106
|
+
|
|
107
|
+
response = HttpResponse(private_key, content_type="application/x-pem-file")
|
|
108
|
+
response["Content-Disposition"] = 'attachment; filename="private.pem"'
|
|
109
|
+
return response
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 2. Логін
|
|
113
|
+
|
|
114
|
+
Форма логіну надсилає:
|
|
115
|
+
|
|
116
|
+
- `username`
|
|
117
|
+
- `password`
|
|
118
|
+
- файл `private_key` або `private_key_file`
|
|
119
|
+
|
|
120
|
+
У view можна зчитати PEM і перевірити його через helper:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from django.contrib.auth import login
|
|
124
|
+
from django.shortcuts import redirect
|
|
125
|
+
|
|
126
|
+
from ecp_lib.auth import authenticate_with_private_key, read_private_key
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def login_view(request):
|
|
130
|
+
if request.method == "POST":
|
|
131
|
+
private_key = read_private_key(
|
|
132
|
+
request.FILES["private_key_file"]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
user, error = authenticate_with_private_key(
|
|
136
|
+
request=request,
|
|
137
|
+
username=request.POST["username"],
|
|
138
|
+
password=request.POST["password"],
|
|
139
|
+
private_key=private_key,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if error:
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
login(request, user)
|
|
146
|
+
return redirect("dashboard")
|
|
147
|
+
|
|
148
|
+
...
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`authenticate_with_private_key()`:
|
|
152
|
+
|
|
153
|
+
1. валідовує вхідні дані
|
|
154
|
+
2. перевіряє `username/password` через Django `authenticate()`
|
|
155
|
+
3. бере `public_key` користувача з `ECPKey`
|
|
156
|
+
4. створює службовий payload
|
|
157
|
+
5. підписує його переданим `private_key`
|
|
158
|
+
6. перевіряє підпис через збережений `public_key`
|
|
159
|
+
|
|
160
|
+
Повертає:
|
|
161
|
+
|
|
162
|
+
- `(user, None)` при успіху
|
|
163
|
+
- `(None, "error text")` при помилці
|
|
164
|
+
|
|
165
|
+
## Middleware
|
|
166
|
+
|
|
167
|
+
[`ecp_lib/middleware.py`](/home/toksik/Developer/hackaton/django-pub-sub/ecp_lib/middleware.py) працює як ранній guard для `POST`-запитів.
|
|
168
|
+
|
|
169
|
+
Middleware реагує на запит, якщо бачить:
|
|
170
|
+
|
|
171
|
+
- `username`
|
|
172
|
+
- `password`
|
|
173
|
+
- `private_key` як текстове поле або файл `private_key`
|
|
174
|
+
- або файл `private_key_file`
|
|
175
|
+
|
|
176
|
+
Підтримувані content types:
|
|
177
|
+
|
|
178
|
+
- `application/x-www-form-urlencoded`
|
|
179
|
+
- `multipart/form-data`
|
|
180
|
+
- `application/json`
|
|
181
|
+
|
|
182
|
+
Що воно робить:
|
|
183
|
+
|
|
184
|
+
1. перевіряє, що це `POST`
|
|
185
|
+
2. зчитує `username`, `password` і приватний ключ
|
|
186
|
+
3. знаходить `public_key` користувача в БД
|
|
187
|
+
4. генерує підпис з переданого приватного ключа
|
|
188
|
+
5. перевіряє підпис через `public_key`
|
|
189
|
+
6. при помилці повертає `403`
|
|
190
|
+
7. при успіху пропускає запит далі у view
|
|
191
|
+
|
|
192
|
+
Важливо:
|
|
193
|
+
|
|
194
|
+
- middleware не створює сесію користувача
|
|
195
|
+
- middleware не замінює `django.contrib.auth.login`
|
|
196
|
+
- middleware лише відсікає невалідні запити до того, як вони дійдуть до view
|
|
197
|
+
|
|
198
|
+
## Підключення до Django
|
|
199
|
+
|
|
200
|
+
У `settings.py`:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
INSTALLED_APPS = [
|
|
204
|
+
"django.contrib.auth",
|
|
205
|
+
"django.contrib.contenttypes",
|
|
206
|
+
"ecp_lib",
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
MIDDLEWARE = [
|
|
210
|
+
"...",
|
|
211
|
+
"ecp_lib.middleware.ECPMiddleware",
|
|
212
|
+
]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Після цього виконай міграції:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
python manage.py migrate
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Логування middleware
|
|
222
|
+
|
|
223
|
+
Щоб бачити, чи middleware спрацьовує, додай logger у `settings.py`:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
LOGGING = {
|
|
227
|
+
"version": 1,
|
|
228
|
+
"disable_existing_loggers": False,
|
|
229
|
+
"handlers": {
|
|
230
|
+
"console": {
|
|
231
|
+
"class": "logging.StreamHandler",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
"loggers": {
|
|
235
|
+
"ecp_lib.middleware": {
|
|
236
|
+
"handlers": ["console"],
|
|
237
|
+
"level": "DEBUG",
|
|
238
|
+
"propagate": False,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
У логах буде видно:
|
|
245
|
+
|
|
246
|
+
- що запит зайшов у middleware
|
|
247
|
+
- чому middleware пропустив запит
|
|
248
|
+
- чому middleware відхилив запит
|
|
249
|
+
- чи перевірка пройшла успішно
|
|
250
|
+
|
|
251
|
+
## Криптографічні helper-и
|
|
252
|
+
|
|
253
|
+
### `generate_keys()`
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from ecp_lib.crypto import generate_keys
|
|
257
|
+
|
|
258
|
+
private_key, public_key = generate_keys()
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
- повертає PEM-рядки
|
|
262
|
+
- генерує тільки RSA-ключі
|
|
263
|
+
- мінімальна довжина ключа: `2048`
|
|
264
|
+
|
|
265
|
+
### `sign()` і `verify()`
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from ecp_lib.crypto import sign, verify
|
|
269
|
+
|
|
270
|
+
signature = sign(private_key, "hello")
|
|
271
|
+
is_valid = verify(public_key, "hello", signature)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Бібліотека використовує RSA-PSS + SHA-256.
|
|
275
|
+
|
|
276
|
+
### `create_challenge()` і `verify_challenge()`
|
|
277
|
+
|
|
278
|
+
Це допоміжні helper-и для тестів або локальної перевірки пари ключів.
|
|
279
|
+
Вони не є основою поточного login-flow через HTML-форму.
|
|
280
|
+
|
|
281
|
+
## Валідація
|
|
282
|
+
|
|
283
|
+
[`ecp_lib/validators.py`](/home/toksik/Developer/hackaton/django-pub-sub/ecp_lib/validators.py) містить:
|
|
284
|
+
|
|
285
|
+
- `sanitize(value)`
|
|
286
|
+
- `validate_username(username)`
|
|
287
|
+
- `validate_public_key(public_key)`
|
|
288
|
+
|
|
289
|
+
Перевіряється:
|
|
290
|
+
|
|
291
|
+
- тип і непорожність значення
|
|
292
|
+
- відсутність небезпечних control characters
|
|
293
|
+
- PEM-формат `public_key`
|
|
294
|
+
- RSA-тип ключа
|
|
295
|
+
- мінімальна довжина ключа `2048`
|
|
296
|
+
|
|
297
|
+
## Модель
|
|
298
|
+
|
|
299
|
+
[`ecp_lib/models.py`](/home/toksik/Developer/hackaton/django-pub-sub/ecp_lib/models.py):
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
class ECPKey(models.Model):
|
|
303
|
+
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="ecp_key")
|
|
304
|
+
public_key = models.TextField()
|
|
305
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
На сервері зберігається тільки `public_key`.
|
|
309
|
+
|
|
310
|
+
## Тести
|
|
311
|
+
|
|
312
|
+
Запуск:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
pytest -q
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Покрито:
|
|
319
|
+
|
|
320
|
+
- генерацію ключів
|
|
321
|
+
- підпис і перевірку
|
|
322
|
+
- збереження `public_key`
|
|
323
|
+
- читання `private.pem`
|
|
324
|
+
- helper-и з `auth.py`
|
|
325
|
+
- middleware для form POST
|
|
326
|
+
- middleware для JSON POST
|
|
327
|
+
|
|
328
|
+
## Безпека
|
|
329
|
+
|
|
330
|
+
- не зберігай `private.pem` у БД
|
|
331
|
+
- віддавай `private.pem` користувачу тільки один раз після реєстрації
|
|
332
|
+
- перевіряй, що в БД лежить тільки валідний `public_key`
|
|
333
|
+
- використовуй HTTPS, бо `password` і файл ключа передаються на сервер
|
|
334
|
+
- middleware варто ставити як ранній бар'єр, але не замість перевірки у view
|
ecp_lib-0.1.0/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# ecp-lib
|
|
2
|
+
|
|
3
|
+
`ecp-lib` — Django-бібліотека для входу з `username + password + private.pem`.
|
|
4
|
+
Вона генерує RSA-ключі, зберігає `public_key` користувача і дає middleware, яке перевіряє, що завантажений приватний ключ відповідає ключу в БД.
|
|
5
|
+
|
|
6
|
+
## Що вміє бібліотека
|
|
7
|
+
|
|
8
|
+
- генерувати пару `private_key/public_key`
|
|
9
|
+
- зберігати `public_key` у моделі `ECPKey`
|
|
10
|
+
- читати `private.pem` з upload
|
|
11
|
+
- перевіряти пару `username/password/private_key`
|
|
12
|
+
- відсікати невалідні логін-запити ще в middleware
|
|
13
|
+
- логувати, чи запит взагалі дійшов до middleware і на якому кроці впав
|
|
14
|
+
|
|
15
|
+
## Що бібліотека не робить
|
|
16
|
+
|
|
17
|
+
- не дає готових `views` або `urls`
|
|
18
|
+
- не логінить користувача сама по собі
|
|
19
|
+
- не зберігає `private.pem` на сервері
|
|
20
|
+
- не будує challenge-response протокол
|
|
21
|
+
|
|
22
|
+
## Встановлення
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install ecp-lib
|
|
26
|
+
pip install "ecp-lib[django]"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Публічне API
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from ecp_lib import (
|
|
33
|
+
ECPKey,
|
|
34
|
+
ECPMiddleware,
|
|
35
|
+
authenticate_with_private_key,
|
|
36
|
+
create_challenge,
|
|
37
|
+
create_user_keys,
|
|
38
|
+
generate_keys,
|
|
39
|
+
read_private_key,
|
|
40
|
+
sanitize,
|
|
41
|
+
sign,
|
|
42
|
+
validate_public_key,
|
|
43
|
+
verify,
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Основний Django-flow використовує:
|
|
48
|
+
|
|
49
|
+
- `create_user_keys()`
|
|
50
|
+
- `read_private_key()`
|
|
51
|
+
- `authenticate_with_private_key()`
|
|
52
|
+
- `ECPMiddleware`
|
|
53
|
+
|
|
54
|
+
## Основний flow
|
|
55
|
+
|
|
56
|
+
### 1. Реєстрація
|
|
57
|
+
|
|
58
|
+
Після створення користувача виклич `create_user_keys(user)`.
|
|
59
|
+
Функція:
|
|
60
|
+
|
|
61
|
+
1. генерує нову RSA-пару
|
|
62
|
+
2. зберігає `public_key` у `ECPKey`
|
|
63
|
+
3. повертає `private_key` як PEM-рядок
|
|
64
|
+
|
|
65
|
+
Типовий варіант: віддати цей PEM користувачу як файл `private.pem`.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from django.http import HttpResponse
|
|
69
|
+
|
|
70
|
+
from ecp_lib.auth import create_user_keys
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def register_success_response(user):
|
|
74
|
+
private_key = create_user_keys(user)
|
|
75
|
+
|
|
76
|
+
response = HttpResponse(private_key, content_type="application/x-pem-file")
|
|
77
|
+
response["Content-Disposition"] = 'attachment; filename="private.pem"'
|
|
78
|
+
return response
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Логін
|
|
82
|
+
|
|
83
|
+
Форма логіну надсилає:
|
|
84
|
+
|
|
85
|
+
- `username`
|
|
86
|
+
- `password`
|
|
87
|
+
- файл `private_key` або `private_key_file`
|
|
88
|
+
|
|
89
|
+
У view можна зчитати PEM і перевірити його через helper:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from django.contrib.auth import login
|
|
93
|
+
from django.shortcuts import redirect
|
|
94
|
+
|
|
95
|
+
from ecp_lib.auth import authenticate_with_private_key, read_private_key
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def login_view(request):
|
|
99
|
+
if request.method == "POST":
|
|
100
|
+
private_key = read_private_key(
|
|
101
|
+
request.FILES["private_key_file"]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
user, error = authenticate_with_private_key(
|
|
105
|
+
request=request,
|
|
106
|
+
username=request.POST["username"],
|
|
107
|
+
password=request.POST["password"],
|
|
108
|
+
private_key=private_key,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if error:
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
login(request, user)
|
|
115
|
+
return redirect("dashboard")
|
|
116
|
+
|
|
117
|
+
...
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`authenticate_with_private_key()`:
|
|
121
|
+
|
|
122
|
+
1. валідовує вхідні дані
|
|
123
|
+
2. перевіряє `username/password` через Django `authenticate()`
|
|
124
|
+
3. бере `public_key` користувача з `ECPKey`
|
|
125
|
+
4. створює службовий payload
|
|
126
|
+
5. підписує його переданим `private_key`
|
|
127
|
+
6. перевіряє підпис через збережений `public_key`
|
|
128
|
+
|
|
129
|
+
Повертає:
|
|
130
|
+
|
|
131
|
+
- `(user, None)` при успіху
|
|
132
|
+
- `(None, "error text")` при помилці
|
|
133
|
+
|
|
134
|
+
## Middleware
|
|
135
|
+
|
|
136
|
+
[`ecp_lib/middleware.py`](/home/toksik/Developer/hackaton/django-pub-sub/ecp_lib/middleware.py) працює як ранній guard для `POST`-запитів.
|
|
137
|
+
|
|
138
|
+
Middleware реагує на запит, якщо бачить:
|
|
139
|
+
|
|
140
|
+
- `username`
|
|
141
|
+
- `password`
|
|
142
|
+
- `private_key` як текстове поле або файл `private_key`
|
|
143
|
+
- або файл `private_key_file`
|
|
144
|
+
|
|
145
|
+
Підтримувані content types:
|
|
146
|
+
|
|
147
|
+
- `application/x-www-form-urlencoded`
|
|
148
|
+
- `multipart/form-data`
|
|
149
|
+
- `application/json`
|
|
150
|
+
|
|
151
|
+
Що воно робить:
|
|
152
|
+
|
|
153
|
+
1. перевіряє, що це `POST`
|
|
154
|
+
2. зчитує `username`, `password` і приватний ключ
|
|
155
|
+
3. знаходить `public_key` користувача в БД
|
|
156
|
+
4. генерує підпис з переданого приватного ключа
|
|
157
|
+
5. перевіряє підпис через `public_key`
|
|
158
|
+
6. при помилці повертає `403`
|
|
159
|
+
7. при успіху пропускає запит далі у view
|
|
160
|
+
|
|
161
|
+
Важливо:
|
|
162
|
+
|
|
163
|
+
- middleware не створює сесію користувача
|
|
164
|
+
- middleware не замінює `django.contrib.auth.login`
|
|
165
|
+
- middleware лише відсікає невалідні запити до того, як вони дійдуть до view
|
|
166
|
+
|
|
167
|
+
## Підключення до Django
|
|
168
|
+
|
|
169
|
+
У `settings.py`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
INSTALLED_APPS = [
|
|
173
|
+
"django.contrib.auth",
|
|
174
|
+
"django.contrib.contenttypes",
|
|
175
|
+
"ecp_lib",
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
MIDDLEWARE = [
|
|
179
|
+
"...",
|
|
180
|
+
"ecp_lib.middleware.ECPMiddleware",
|
|
181
|
+
]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Після цього виконай міграції:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python manage.py migrate
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Логування middleware
|
|
191
|
+
|
|
192
|
+
Щоб бачити, чи middleware спрацьовує, додай logger у `settings.py`:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
LOGGING = {
|
|
196
|
+
"version": 1,
|
|
197
|
+
"disable_existing_loggers": False,
|
|
198
|
+
"handlers": {
|
|
199
|
+
"console": {
|
|
200
|
+
"class": "logging.StreamHandler",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
"loggers": {
|
|
204
|
+
"ecp_lib.middleware": {
|
|
205
|
+
"handlers": ["console"],
|
|
206
|
+
"level": "DEBUG",
|
|
207
|
+
"propagate": False,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
У логах буде видно:
|
|
214
|
+
|
|
215
|
+
- що запит зайшов у middleware
|
|
216
|
+
- чому middleware пропустив запит
|
|
217
|
+
- чому middleware відхилив запит
|
|
218
|
+
- чи перевірка пройшла успішно
|
|
219
|
+
|
|
220
|
+
## Криптографічні helper-и
|
|
221
|
+
|
|
222
|
+
### `generate_keys()`
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from ecp_lib.crypto import generate_keys
|
|
226
|
+
|
|
227
|
+
private_key, public_key = generate_keys()
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- повертає PEM-рядки
|
|
231
|
+
- генерує тільки RSA-ключі
|
|
232
|
+
- мінімальна довжина ключа: `2048`
|
|
233
|
+
|
|
234
|
+
### `sign()` і `verify()`
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from ecp_lib.crypto import sign, verify
|
|
238
|
+
|
|
239
|
+
signature = sign(private_key, "hello")
|
|
240
|
+
is_valid = verify(public_key, "hello", signature)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Бібліотека використовує RSA-PSS + SHA-256.
|
|
244
|
+
|
|
245
|
+
### `create_challenge()` і `verify_challenge()`
|
|
246
|
+
|
|
247
|
+
Це допоміжні helper-и для тестів або локальної перевірки пари ключів.
|
|
248
|
+
Вони не є основою поточного login-flow через HTML-форму.
|
|
249
|
+
|
|
250
|
+
## Валідація
|
|
251
|
+
|
|
252
|
+
[`ecp_lib/validators.py`](/home/toksik/Developer/hackaton/django-pub-sub/ecp_lib/validators.py) містить:
|
|
253
|
+
|
|
254
|
+
- `sanitize(value)`
|
|
255
|
+
- `validate_username(username)`
|
|
256
|
+
- `validate_public_key(public_key)`
|
|
257
|
+
|
|
258
|
+
Перевіряється:
|
|
259
|
+
|
|
260
|
+
- тип і непорожність значення
|
|
261
|
+
- відсутність небезпечних control characters
|
|
262
|
+
- PEM-формат `public_key`
|
|
263
|
+
- RSA-тип ключа
|
|
264
|
+
- мінімальна довжина ключа `2048`
|
|
265
|
+
|
|
266
|
+
## Модель
|
|
267
|
+
|
|
268
|
+
[`ecp_lib/models.py`](/home/toksik/Developer/hackaton/django-pub-sub/ecp_lib/models.py):
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
class ECPKey(models.Model):
|
|
272
|
+
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="ecp_key")
|
|
273
|
+
public_key = models.TextField()
|
|
274
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
На сервері зберігається тільки `public_key`.
|
|
278
|
+
|
|
279
|
+
## Тести
|
|
280
|
+
|
|
281
|
+
Запуск:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
pytest -q
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Покрито:
|
|
288
|
+
|
|
289
|
+
- генерацію ключів
|
|
290
|
+
- підпис і перевірку
|
|
291
|
+
- збереження `public_key`
|
|
292
|
+
- читання `private.pem`
|
|
293
|
+
- helper-и з `auth.py`
|
|
294
|
+
- middleware для form POST
|
|
295
|
+
- middleware для JSON POST
|
|
296
|
+
|
|
297
|
+
## Безпека
|
|
298
|
+
|
|
299
|
+
- не зберігай `private.pem` у БД
|
|
300
|
+
- віддавай `private.pem` користувачу тільки один раз після реєстрації
|
|
301
|
+
- перевіряй, що в БД лежить тільки валідний `public_key`
|
|
302
|
+
- використовуй HTTPS, бо `password` і файл ключа передаються на сервер
|
|
303
|
+
- middleware варто ставити як ранній бар'єр, але не замість перевірки у view
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
|
|
5
|
+
# Кореневий модуль нічого не імпортує напряму, а працює через lazy imports.
|
|
6
|
+
# Це важливо для Django, щоб пакет безпечно підключався через INSTALLED_APPS
|
|
7
|
+
# і не тягнув моделі занадто рано під час apps.populate().
|
|
8
|
+
__all__ = [
|
|
9
|
+
"authenticate_with_private_key",
|
|
10
|
+
"create_challenge",
|
|
11
|
+
"create_user_keys",
|
|
12
|
+
"ECPKey",
|
|
13
|
+
"ECPMiddleware",
|
|
14
|
+
"generate_keys",
|
|
15
|
+
"read_private_key",
|
|
16
|
+
"sanitize",
|
|
17
|
+
"sign",
|
|
18
|
+
"validate_public_key",
|
|
19
|
+
"verify",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
_EXPORTS = {
|
|
23
|
+
"authenticate_with_private_key": ("ecp_lib.auth", "authenticate_with_private_key"),
|
|
24
|
+
"create_challenge": ("ecp_lib.auth", "create_challenge"),
|
|
25
|
+
"create_user_keys": ("ecp_lib.auth", "create_user_keys"),
|
|
26
|
+
"ECPKey": ("ecp_lib.models", "ECPKey"),
|
|
27
|
+
"ECPMiddleware": ("ecp_lib.middleware", "ECPMiddleware"),
|
|
28
|
+
"generate_keys": ("ecp_lib.crypto", "generate_keys"),
|
|
29
|
+
"read_private_key": ("ecp_lib.auth", "read_private_key"),
|
|
30
|
+
"sanitize": ("ecp_lib.validators", "sanitize"),
|
|
31
|
+
"sign": ("ecp_lib.crypto", "sign"),
|
|
32
|
+
"validate_public_key": ("ecp_lib.validators", "validate_public_key"),
|
|
33
|
+
"verify": ("ecp_lib.crypto", "verify"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def __getattr__(name: str):
|
|
38
|
+
# Ледачий імпорт дозволяє працювати з API як з "плоским" пакетом,
|
|
39
|
+
# але не створює проблем під час старту Django.
|
|
40
|
+
if name not in _EXPORTS:
|
|
41
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
42
|
+
|
|
43
|
+
module_name, attr_name = _EXPORTS[name]
|
|
44
|
+
value = getattr(import_module(module_name), attr_name)
|
|
45
|
+
globals()[name] = value
|
|
46
|
+
return value
|