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 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.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include ecp_lib/migrations *.py
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
@@ -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