alea-q 0.2.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.
- alea_q-0.2.0/.gitignore +9 -0
- alea_q-0.2.0/PKG-INFO +349 -0
- alea_q-0.2.0/README.md +315 -0
- alea_q-0.2.0/alea_q/__init__.py +83 -0
- alea_q-0.2.0/alea_q/_http.py +220 -0
- alea_q-0.2.0/alea_q/client.py +181 -0
- alea_q-0.2.0/alea_q/exceptions.py +120 -0
- alea_q-0.2.0/alea_q/models.py +412 -0
- alea_q-0.2.0/alea_q/pqc_crypto.py +143 -0
- alea_q-0.2.0/alea_q/tiers/__init__.py +0 -0
- alea_q-0.2.0/alea_q/tiers/ivory.py +98 -0
- alea_q-0.2.0/alea_q/tiers/platinum.py +129 -0
- alea_q-0.2.0/alea_q/tiers/pqc.py +273 -0
- alea_q-0.2.0/alea_q/tiers/silver.py +216 -0
- alea_q-0.2.0/alea_q/verify.py +200 -0
- alea_q-0.2.0/pyproject.toml +73 -0
- alea_q-0.2.0/tests/__init__.py +0 -0
- alea_q-0.2.0/tests/alea_q_test_client.py +911 -0
- alea_q-0.2.0/tests/alea_q_test_pqc_crypto.py +117 -0
- alea_q-0.2.0/tests/alea_q_test_smoke.py +96 -0
- alea_q-0.2.0/tests/alea_q_test_stream.py +179 -0
alea_q-0.2.0/.gitignore
ADDED
alea_q-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: alea-q
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: SDK Python pour l'API ALEA-Q / CERTROPI — génération d'entiers aléatoires certifiés Device-Independent
|
|
5
|
+
Project-URL: Homepage, https://alea-q.com
|
|
6
|
+
Project-URL: Documentation, https://docs.alea-q.com
|
|
7
|
+
Project-URL: Repository, https://gitlab.com/certropi/alea-q-python
|
|
8
|
+
Project-URL: Bug Tracker, https://gitlab.com/certropi/alea-q-python/-/issues
|
|
9
|
+
Author-email: CERTROPI <sdk@certropi.com>
|
|
10
|
+
License: Proprietary
|
|
11
|
+
Keywords: alea-q,certropi,cryptography,device-independent,qrng,quantum,random
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
19
|
+
Classifier: Topic :: Security :: Cryptography
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: cryptography>=47.0.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: websockets>=12.0; extra == 'dev'
|
|
29
|
+
Provides-Extra: stream
|
|
30
|
+
Requires-Dist: websockets>=12.0; extra == 'stream'
|
|
31
|
+
Provides-Extra: verify
|
|
32
|
+
Requires-Dist: cryptography>=47.0.0; extra == 'verify'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# alea-q
|
|
36
|
+
|
|
37
|
+
SDK Python officiel pour l'API **ALEA-Q / CERTROPI** — génération d'entiers aléatoires certifiés Device-Independent depuis des QPU réels.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install alea-q
|
|
43
|
+
|
|
44
|
+
# Avec vérification de certificats d'attestation
|
|
45
|
+
pip install "alea-q[verify]"
|
|
46
|
+
|
|
47
|
+
# Avec streaming Silver en temps réel
|
|
48
|
+
pip install "alea-q[stream]"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Démarrage rapide
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from alea_q import AleaQClient
|
|
55
|
+
|
|
56
|
+
# Clé API dans le constructeur ou variable d'env ALEA_Q_API_KEY
|
|
57
|
+
client = AleaQClient(api_key="sk_plat_xxx")
|
|
58
|
+
|
|
59
|
+
# Platinum — entier DI-certifié depuis QPU réel
|
|
60
|
+
result = client.platinum.generate(n_bits=256)
|
|
61
|
+
print(result.as_hex()) # 0x3f2a...
|
|
62
|
+
print(result.S) # 2.735 (valeur CHSH)
|
|
63
|
+
print(result.certificate) # dict d'attestation SHA-3/ECDSA
|
|
64
|
+
|
|
65
|
+
# Silver — DRBG seedé QPU
|
|
66
|
+
result = client.silver.generate(n_bits=128)
|
|
67
|
+
|
|
68
|
+
# Ivory — batch haute vitesse
|
|
69
|
+
batch = client.ivory.batch(size=10000, n_bits=32)
|
|
70
|
+
print(batch.values[:5])
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Authentification
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Option 1 — clé dans le constructeur
|
|
77
|
+
client = AleaQClient(api_key="sk_plat_xxx")
|
|
78
|
+
|
|
79
|
+
# Option 2 — variable d'environnement
|
|
80
|
+
import os
|
|
81
|
+
os.environ["ALEA_Q_API_KEY"] = "sk_plat_xxx"
|
|
82
|
+
client = AleaQClient()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Le tier est détecté automatiquement depuis le préfixe de la clé
|
|
86
|
+
(`sk_ivor_` / `sk_silv_` / `sk_plat_`) :
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
client = AleaQClient(api_key="sk_plat_xxx")
|
|
90
|
+
print(client.tier) # "platinum"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Compte authentifié
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
info = client.me()
|
|
97
|
+
print(info.tier) # "platinum"
|
|
98
|
+
print(info.quota.remaining_today) # entiers restants aujourd'hui
|
|
99
|
+
print(info.usage.total_calls) # appels cumulés
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Tiers disponibles
|
|
103
|
+
|
|
104
|
+
| Tier | Source | Certification | Latence |
|
|
105
|
+
|----------|---------------------------------|---------------------|------------|
|
|
106
|
+
| Platinum | QPU réel (IBM, Quandela, Pasqal)| DI Bell-CHSH | < 1ms pool |
|
|
107
|
+
| Silver | DRBG seedé QPU | Seed quantique | < 1ms |
|
|
108
|
+
| Ivory | Simulateur Toeplitz | Haute qualité stat. | < 0.1ms |
|
|
109
|
+
|
|
110
|
+
## Vérification des certificats Platinum
|
|
111
|
+
|
|
112
|
+
Chaque réponse Platinum inclut un certificat d'attestation vérifiable sans contacter CERTROPI.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# Vérification automatique à la génération
|
|
116
|
+
result = client.platinum.generate(
|
|
117
|
+
n_bits=256,
|
|
118
|
+
verify=True,
|
|
119
|
+
pubkey_path="certropi_public.pem",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Vérification manuelle depuis un dict
|
|
123
|
+
from alea_q import verify_certificate_dict
|
|
124
|
+
S = verify_certificate_dict(result.certificate, pubkey_path="certropi_public.pem")
|
|
125
|
+
print(f"Certificat valide — S={S:.4f}")
|
|
126
|
+
|
|
127
|
+
# Vérification depuis un fichier JSON
|
|
128
|
+
from alea_q import verify_certificate
|
|
129
|
+
verify_certificate("cert.json", pubkey_path="certropi_public.pem")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Récupérer la clé publique CERTROPI :
|
|
133
|
+
```bash
|
|
134
|
+
curl https://api.alea-q.com/v1/platinum/pubkey -o certropi_public.pem
|
|
135
|
+
# ou
|
|
136
|
+
python -c "
|
|
137
|
+
from alea_q import AleaQClient
|
|
138
|
+
client = AleaQClient()
|
|
139
|
+
open('certropi_public.pem', 'wb').write(client.platinum.get_public_key())
|
|
140
|
+
"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Context manager
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
with AleaQClient(api_key="sk_plat_xxx") as client:
|
|
147
|
+
result = client.platinum.generate(n_bits=256)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Client asynchrone
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import asyncio
|
|
154
|
+
from alea_q import AsyncAleaQClient
|
|
155
|
+
|
|
156
|
+
async def main():
|
|
157
|
+
async with AsyncAleaQClient(api_key="sk_plat_xxx") as client:
|
|
158
|
+
result = await client.platinum.generate(n_bits=256)
|
|
159
|
+
print(result.as_hex())
|
|
160
|
+
|
|
161
|
+
asyncio.run(main())
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Streaming Silver (temps réel)
|
|
165
|
+
|
|
166
|
+
Flux continu d'entiers en WebSocket — utile pour alimenter en continu un
|
|
167
|
+
consommateur d'entropie (HSM, pool applicatif, génération de clés en
|
|
168
|
+
masse) sans refaire une requête HTTP par lot.
|
|
169
|
+
|
|
170
|
+
Nécessite l'extra `stream` : `pip install "alea-q[stream]"`
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import asyncio
|
|
174
|
+
from alea_q import AsyncAleaQClient
|
|
175
|
+
|
|
176
|
+
async def main():
|
|
177
|
+
async with AsyncAleaQClient(api_key="sk_silv_xxx") as client:
|
|
178
|
+
count = 0
|
|
179
|
+
async for value in client.silver.stream(bits_per_second=100_000, n_bits=256):
|
|
180
|
+
print(value)
|
|
181
|
+
count += 1
|
|
182
|
+
if count >= 1000:
|
|
183
|
+
break # ferme proprement le flux côté client
|
|
184
|
+
|
|
185
|
+
asyncio.run(main())
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Le débit est exprimé en **bits/seconde**, pas en nombre d'entiers — cohérent
|
|
189
|
+
avec la facturation volumétrique du service et indépendant de `n_bits`
|
|
190
|
+
(la largeur de chaque entier livré). Le flux reste ouvert tant que la
|
|
191
|
+
boucle `async for` n'est pas interrompue (`break`) et que la clé reste
|
|
192
|
+
valide — une révocation de clé en cours de flux le referme immédiatement.
|
|
193
|
+
|
|
194
|
+
Gestion des erreurs spécifiques au streaming :
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from alea_q import AuthenticationError, InsufficientBalance, QuotaExceeded
|
|
198
|
+
|
|
199
|
+
async def main():
|
|
200
|
+
async with AsyncAleaQClient(api_key="sk_silv_xxx") as client:
|
|
201
|
+
try:
|
|
202
|
+
async for value in client.silver.stream(bits_per_second=100_000):
|
|
203
|
+
...
|
|
204
|
+
except AuthenticationError:
|
|
205
|
+
print("Clé invalide ou révoquée en cours de flux")
|
|
206
|
+
except InsufficientBalance:
|
|
207
|
+
print("Solde prépayé épuisé — recharger via le support")
|
|
208
|
+
except QuotaExceeded:
|
|
209
|
+
print("Capacité de débit disponible dépassée — réessayer plus tard")
|
|
210
|
+
|
|
211
|
+
asyncio.run(main())
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Le client synchrone (`AleaQClient`) ne propose pas le streaming — le
|
|
215
|
+
flux est intrinsèquement asynchrone, utiliser `AsyncAleaQClient`.
|
|
216
|
+
|
|
217
|
+
## Post-quantique (ML-KEM / SLH-DSA)
|
|
218
|
+
|
|
219
|
+
Génération de clés post-quantiques (FIPS 203/205) depuis une graine
|
|
220
|
+
QRNG-DI certifiée, signature et vérification SLH-DSA — disponible sur
|
|
221
|
+
les tiers Silver et Platinum (`client.silver.*` / `client.platinum.*`,
|
|
222
|
+
mêmes méthodes ; Ivory n'a pas accès aux routes PQC).
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
# ML-KEM (FIPS 203) — encapsulation de clé, round-trip complet
|
|
226
|
+
kp = client.platinum.mlkem_keygen(param_set="ML_KEM_768")
|
|
227
|
+
print(kp.ek, kp.dk)
|
|
228
|
+
|
|
229
|
+
encaps = client.platinum.mlkem_encaps(ek=kp.ek)
|
|
230
|
+
print(encaps.shared_key, encaps.ciphertext)
|
|
231
|
+
|
|
232
|
+
# Decaps est une opération LOCALE (le serveur ne voit jamais dk) —
|
|
233
|
+
# nécessite l'extra 'verify' : pip install 'alea-q[verify]'
|
|
234
|
+
from alea_q import mlkem_decaps
|
|
235
|
+
|
|
236
|
+
shared_key = mlkem_decaps(kp.dk, encaps.ciphertext, param_set=kp.param_set)
|
|
237
|
+
assert shared_key.hex() == encaps.shared_key # même secret des deux côtés
|
|
238
|
+
|
|
239
|
+
# SLH-DSA (FIPS 205) — génération de clé, signature, vérification
|
|
240
|
+
sig_kp = client.platinum.slhdsa_keygen(param_set="SLH_DSA_SHAKE_256s")
|
|
241
|
+
print(sig_kp.sk, sig_kp.pk)
|
|
242
|
+
|
|
243
|
+
signed = client.platinum.slhdsa_sign(
|
|
244
|
+
sk=sig_kp.sk,
|
|
245
|
+
message="document à signer",
|
|
246
|
+
param_set="SLH_DSA_SHAKE_256s",
|
|
247
|
+
)
|
|
248
|
+
print(signed.signature, signed.sig_size)
|
|
249
|
+
|
|
250
|
+
verified = client.platinum.slhdsa_verify(
|
|
251
|
+
pk=sig_kp.pk,
|
|
252
|
+
signature=signed.signature,
|
|
253
|
+
message="document à signer",
|
|
254
|
+
param_set="SLH_DSA_SHAKE_256s",
|
|
255
|
+
)
|
|
256
|
+
print(verified.valid) # True — ne lève pas d'exception si False
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Seule la génération de clé (`mlkem_keygen()` / `slhdsa_keygen()`)
|
|
260
|
+
consomme une graine QRNG-DI certifiée. `slhdsa_sign()` /
|
|
261
|
+
`slhdsa_verify()` sont des opérations crypto pures côté serveur sur
|
|
262
|
+
des clés déjà fournies par le client — chaque appel fait bien un
|
|
263
|
+
round-trip réseau (le SDK ne recalcule rien localement), mais aucun
|
|
264
|
+
des deux n'entame de quota d'entropie.
|
|
265
|
+
|
|
266
|
+
### Livraison sécurisée de clé privée (Platinum, `recipient_pk`)
|
|
267
|
+
|
|
268
|
+
Pour ne jamais faire transiter une clé privée en clair, `mlkem_keygen()`
|
|
269
|
+
et `slhdsa_keygen()` acceptent (Platinum uniquement) un `recipient_pk` —
|
|
270
|
+
la clé privée générée revient alors enveloppée (ML-KEM + AES-256-GCM +
|
|
271
|
+
signature SLH-DSA) dans `.envelope` au lieu d'être renvoyée en clair
|
|
272
|
+
dans `.dk` / `.sk` :
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
import base64
|
|
276
|
+
from alea_q import mlkem_decaps
|
|
277
|
+
|
|
278
|
+
kp = client.platinum.slhdsa_keygen(recipient_pk=my_mlkem_pubkey_hex)
|
|
279
|
+
if kp.envelope:
|
|
280
|
+
# kp.sk est vide — la clé réelle est dans kp.envelope["encrypted_payload"],
|
|
281
|
+
# chiffrée pour mlkem_pubkey_hex. sk_bob_dk = dk ML-KEM de Bob (obtenu
|
|
282
|
+
# via son propre mlkem_keygen()) — la décapsulation reste locale, le
|
|
283
|
+
# serveur ne voit jamais ni sk_bob_dk ni le secret déchiffré.
|
|
284
|
+
#
|
|
285
|
+
# ATTENTION : les champs de l'enveloppe (ciphertext, encrypted_payload,
|
|
286
|
+
# nonce, signature, certropi_pk) sont en BASE64 — contrairement à
|
|
287
|
+
# dk/ek/shared_key (mlkem_keygen()/mlkem_encaps()) qui sont en HEX.
|
|
288
|
+
ciphertext = base64.b64decode(kp.envelope["ciphertext"])
|
|
289
|
+
shared_key = mlkem_decaps(sk_bob_dk, ciphertext, param_set="ML_KEM_768")
|
|
290
|
+
# shared_key (32 octets) est la clé AES-256-GCM utilisée pour chiffrer
|
|
291
|
+
# kp.envelope["encrypted_payload"] (nonce en base64 lui aussi) —
|
|
292
|
+
# déchiffrement AES-GCM restant à la charge de l'appelant, non fourni
|
|
293
|
+
# par ce SDK à ce jour.
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Gestion des erreurs
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
from alea_q import AleaQClient
|
|
300
|
+
from alea_q.exceptions import BackendUnavailable, AuthenticationError, InsufficientBalance
|
|
301
|
+
import time
|
|
302
|
+
|
|
303
|
+
client = AleaQClient()
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
result = client.platinum.generate(n_bits=256)
|
|
307
|
+
except BackendUnavailable as e:
|
|
308
|
+
print(f"QPU indisponible — réessayer dans {e.retry_after}s")
|
|
309
|
+
time.sleep(e.retry_after or 300)
|
|
310
|
+
except AuthenticationError:
|
|
311
|
+
print("Clé API invalide")
|
|
312
|
+
except InsufficientBalance:
|
|
313
|
+
print("Solde prépayé épuisé (mode pay-as-you-go) — recharger via le support")
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Configuration
|
|
317
|
+
|
|
318
|
+
| Variable d'env | Description | Défaut |
|
|
319
|
+
|----------------------|------------------------------------------|-----------------------------|
|
|
320
|
+
| `ALEA_Q_API_KEY` | Clé API | — |
|
|
321
|
+
| `ALEA_Q_BASE_URL` | URL de base de l'API | `https://api.alea-q.com` |
|
|
322
|
+
| `ALEA_Q_TIMEOUT` | Timeout HTTP en secondes | `60` |
|
|
323
|
+
| `ALEA_Q_MAX_RETRIES` | Nombre de retries automatiques | `2` |
|
|
324
|
+
| `ALEA_Q_PUBKEY_PATH` | Chemin clé publique CERTROPI | `certropi_public.pem` |
|
|
325
|
+
|
|
326
|
+
`https://api.certropi.com` est un alias B2B de la même API — les deux
|
|
327
|
+
domaines répondent de manière identique, `ALEA_Q_BASE_URL` accepte
|
|
328
|
+
indifféremment l'un ou l'autre.
|
|
329
|
+
|
|
330
|
+
## Développement
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
pip install -e ".[dev]"
|
|
334
|
+
|
|
335
|
+
# Suite de tests (mock HTTP via respx — aucun accès réseau requis)
|
|
336
|
+
pytest
|
|
337
|
+
|
|
338
|
+
# Avec couverture
|
|
339
|
+
pytest --cov=alea_q --cov-report=term-missing
|
|
340
|
+
|
|
341
|
+
# Tests contre l'API réelle (marqués `smoke`, nécessitent ALEA_Q_API_KEY
|
|
342
|
+
# et un accès réseau — exclus par défaut)
|
|
343
|
+
pytest -m smoke
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Licence
|
|
347
|
+
|
|
348
|
+
Propriétaire — CERTROPI © 2026
|
|
349
|
+
|
alea_q-0.2.0/README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# alea-q
|
|
2
|
+
|
|
3
|
+
SDK Python officiel pour l'API **ALEA-Q / CERTROPI** — génération d'entiers aléatoires certifiés Device-Independent depuis des QPU réels.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install alea-q
|
|
9
|
+
|
|
10
|
+
# Avec vérification de certificats d'attestation
|
|
11
|
+
pip install "alea-q[verify]"
|
|
12
|
+
|
|
13
|
+
# Avec streaming Silver en temps réel
|
|
14
|
+
pip install "alea-q[stream]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Démarrage rapide
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from alea_q import AleaQClient
|
|
21
|
+
|
|
22
|
+
# Clé API dans le constructeur ou variable d'env ALEA_Q_API_KEY
|
|
23
|
+
client = AleaQClient(api_key="sk_plat_xxx")
|
|
24
|
+
|
|
25
|
+
# Platinum — entier DI-certifié depuis QPU réel
|
|
26
|
+
result = client.platinum.generate(n_bits=256)
|
|
27
|
+
print(result.as_hex()) # 0x3f2a...
|
|
28
|
+
print(result.S) # 2.735 (valeur CHSH)
|
|
29
|
+
print(result.certificate) # dict d'attestation SHA-3/ECDSA
|
|
30
|
+
|
|
31
|
+
# Silver — DRBG seedé QPU
|
|
32
|
+
result = client.silver.generate(n_bits=128)
|
|
33
|
+
|
|
34
|
+
# Ivory — batch haute vitesse
|
|
35
|
+
batch = client.ivory.batch(size=10000, n_bits=32)
|
|
36
|
+
print(batch.values[:5])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Authentification
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# Option 1 — clé dans le constructeur
|
|
43
|
+
client = AleaQClient(api_key="sk_plat_xxx")
|
|
44
|
+
|
|
45
|
+
# Option 2 — variable d'environnement
|
|
46
|
+
import os
|
|
47
|
+
os.environ["ALEA_Q_API_KEY"] = "sk_plat_xxx"
|
|
48
|
+
client = AleaQClient()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Le tier est détecté automatiquement depuis le préfixe de la clé
|
|
52
|
+
(`sk_ivor_` / `sk_silv_` / `sk_plat_`) :
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
client = AleaQClient(api_key="sk_plat_xxx")
|
|
56
|
+
print(client.tier) # "platinum"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Compte authentifié
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
info = client.me()
|
|
63
|
+
print(info.tier) # "platinum"
|
|
64
|
+
print(info.quota.remaining_today) # entiers restants aujourd'hui
|
|
65
|
+
print(info.usage.total_calls) # appels cumulés
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Tiers disponibles
|
|
69
|
+
|
|
70
|
+
| Tier | Source | Certification | Latence |
|
|
71
|
+
|----------|---------------------------------|---------------------|------------|
|
|
72
|
+
| Platinum | QPU réel (IBM, Quandela, Pasqal)| DI Bell-CHSH | < 1ms pool |
|
|
73
|
+
| Silver | DRBG seedé QPU | Seed quantique | < 1ms |
|
|
74
|
+
| Ivory | Simulateur Toeplitz | Haute qualité stat. | < 0.1ms |
|
|
75
|
+
|
|
76
|
+
## Vérification des certificats Platinum
|
|
77
|
+
|
|
78
|
+
Chaque réponse Platinum inclut un certificat d'attestation vérifiable sans contacter CERTROPI.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# Vérification automatique à la génération
|
|
82
|
+
result = client.platinum.generate(
|
|
83
|
+
n_bits=256,
|
|
84
|
+
verify=True,
|
|
85
|
+
pubkey_path="certropi_public.pem",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Vérification manuelle depuis un dict
|
|
89
|
+
from alea_q import verify_certificate_dict
|
|
90
|
+
S = verify_certificate_dict(result.certificate, pubkey_path="certropi_public.pem")
|
|
91
|
+
print(f"Certificat valide — S={S:.4f}")
|
|
92
|
+
|
|
93
|
+
# Vérification depuis un fichier JSON
|
|
94
|
+
from alea_q import verify_certificate
|
|
95
|
+
verify_certificate("cert.json", pubkey_path="certropi_public.pem")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Récupérer la clé publique CERTROPI :
|
|
99
|
+
```bash
|
|
100
|
+
curl https://api.alea-q.com/v1/platinum/pubkey -o certropi_public.pem
|
|
101
|
+
# ou
|
|
102
|
+
python -c "
|
|
103
|
+
from alea_q import AleaQClient
|
|
104
|
+
client = AleaQClient()
|
|
105
|
+
open('certropi_public.pem', 'wb').write(client.platinum.get_public_key())
|
|
106
|
+
"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Context manager
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
with AleaQClient(api_key="sk_plat_xxx") as client:
|
|
113
|
+
result = client.platinum.generate(n_bits=256)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Client asynchrone
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import asyncio
|
|
120
|
+
from alea_q import AsyncAleaQClient
|
|
121
|
+
|
|
122
|
+
async def main():
|
|
123
|
+
async with AsyncAleaQClient(api_key="sk_plat_xxx") as client:
|
|
124
|
+
result = await client.platinum.generate(n_bits=256)
|
|
125
|
+
print(result.as_hex())
|
|
126
|
+
|
|
127
|
+
asyncio.run(main())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Streaming Silver (temps réel)
|
|
131
|
+
|
|
132
|
+
Flux continu d'entiers en WebSocket — utile pour alimenter en continu un
|
|
133
|
+
consommateur d'entropie (HSM, pool applicatif, génération de clés en
|
|
134
|
+
masse) sans refaire une requête HTTP par lot.
|
|
135
|
+
|
|
136
|
+
Nécessite l'extra `stream` : `pip install "alea-q[stream]"`
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import asyncio
|
|
140
|
+
from alea_q import AsyncAleaQClient
|
|
141
|
+
|
|
142
|
+
async def main():
|
|
143
|
+
async with AsyncAleaQClient(api_key="sk_silv_xxx") as client:
|
|
144
|
+
count = 0
|
|
145
|
+
async for value in client.silver.stream(bits_per_second=100_000, n_bits=256):
|
|
146
|
+
print(value)
|
|
147
|
+
count += 1
|
|
148
|
+
if count >= 1000:
|
|
149
|
+
break # ferme proprement le flux côté client
|
|
150
|
+
|
|
151
|
+
asyncio.run(main())
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Le débit est exprimé en **bits/seconde**, pas en nombre d'entiers — cohérent
|
|
155
|
+
avec la facturation volumétrique du service et indépendant de `n_bits`
|
|
156
|
+
(la largeur de chaque entier livré). Le flux reste ouvert tant que la
|
|
157
|
+
boucle `async for` n'est pas interrompue (`break`) et que la clé reste
|
|
158
|
+
valide — une révocation de clé en cours de flux le referme immédiatement.
|
|
159
|
+
|
|
160
|
+
Gestion des erreurs spécifiques au streaming :
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from alea_q import AuthenticationError, InsufficientBalance, QuotaExceeded
|
|
164
|
+
|
|
165
|
+
async def main():
|
|
166
|
+
async with AsyncAleaQClient(api_key="sk_silv_xxx") as client:
|
|
167
|
+
try:
|
|
168
|
+
async for value in client.silver.stream(bits_per_second=100_000):
|
|
169
|
+
...
|
|
170
|
+
except AuthenticationError:
|
|
171
|
+
print("Clé invalide ou révoquée en cours de flux")
|
|
172
|
+
except InsufficientBalance:
|
|
173
|
+
print("Solde prépayé épuisé — recharger via le support")
|
|
174
|
+
except QuotaExceeded:
|
|
175
|
+
print("Capacité de débit disponible dépassée — réessayer plus tard")
|
|
176
|
+
|
|
177
|
+
asyncio.run(main())
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Le client synchrone (`AleaQClient`) ne propose pas le streaming — le
|
|
181
|
+
flux est intrinsèquement asynchrone, utiliser `AsyncAleaQClient`.
|
|
182
|
+
|
|
183
|
+
## Post-quantique (ML-KEM / SLH-DSA)
|
|
184
|
+
|
|
185
|
+
Génération de clés post-quantiques (FIPS 203/205) depuis une graine
|
|
186
|
+
QRNG-DI certifiée, signature et vérification SLH-DSA — disponible sur
|
|
187
|
+
les tiers Silver et Platinum (`client.silver.*` / `client.platinum.*`,
|
|
188
|
+
mêmes méthodes ; Ivory n'a pas accès aux routes PQC).
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
# ML-KEM (FIPS 203) — encapsulation de clé, round-trip complet
|
|
192
|
+
kp = client.platinum.mlkem_keygen(param_set="ML_KEM_768")
|
|
193
|
+
print(kp.ek, kp.dk)
|
|
194
|
+
|
|
195
|
+
encaps = client.platinum.mlkem_encaps(ek=kp.ek)
|
|
196
|
+
print(encaps.shared_key, encaps.ciphertext)
|
|
197
|
+
|
|
198
|
+
# Decaps est une opération LOCALE (le serveur ne voit jamais dk) —
|
|
199
|
+
# nécessite l'extra 'verify' : pip install 'alea-q[verify]'
|
|
200
|
+
from alea_q import mlkem_decaps
|
|
201
|
+
|
|
202
|
+
shared_key = mlkem_decaps(kp.dk, encaps.ciphertext, param_set=kp.param_set)
|
|
203
|
+
assert shared_key.hex() == encaps.shared_key # même secret des deux côtés
|
|
204
|
+
|
|
205
|
+
# SLH-DSA (FIPS 205) — génération de clé, signature, vérification
|
|
206
|
+
sig_kp = client.platinum.slhdsa_keygen(param_set="SLH_DSA_SHAKE_256s")
|
|
207
|
+
print(sig_kp.sk, sig_kp.pk)
|
|
208
|
+
|
|
209
|
+
signed = client.platinum.slhdsa_sign(
|
|
210
|
+
sk=sig_kp.sk,
|
|
211
|
+
message="document à signer",
|
|
212
|
+
param_set="SLH_DSA_SHAKE_256s",
|
|
213
|
+
)
|
|
214
|
+
print(signed.signature, signed.sig_size)
|
|
215
|
+
|
|
216
|
+
verified = client.platinum.slhdsa_verify(
|
|
217
|
+
pk=sig_kp.pk,
|
|
218
|
+
signature=signed.signature,
|
|
219
|
+
message="document à signer",
|
|
220
|
+
param_set="SLH_DSA_SHAKE_256s",
|
|
221
|
+
)
|
|
222
|
+
print(verified.valid) # True — ne lève pas d'exception si False
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Seule la génération de clé (`mlkem_keygen()` / `slhdsa_keygen()`)
|
|
226
|
+
consomme une graine QRNG-DI certifiée. `slhdsa_sign()` /
|
|
227
|
+
`slhdsa_verify()` sont des opérations crypto pures côté serveur sur
|
|
228
|
+
des clés déjà fournies par le client — chaque appel fait bien un
|
|
229
|
+
round-trip réseau (le SDK ne recalcule rien localement), mais aucun
|
|
230
|
+
des deux n'entame de quota d'entropie.
|
|
231
|
+
|
|
232
|
+
### Livraison sécurisée de clé privée (Platinum, `recipient_pk`)
|
|
233
|
+
|
|
234
|
+
Pour ne jamais faire transiter une clé privée en clair, `mlkem_keygen()`
|
|
235
|
+
et `slhdsa_keygen()` acceptent (Platinum uniquement) un `recipient_pk` —
|
|
236
|
+
la clé privée générée revient alors enveloppée (ML-KEM + AES-256-GCM +
|
|
237
|
+
signature SLH-DSA) dans `.envelope` au lieu d'être renvoyée en clair
|
|
238
|
+
dans `.dk` / `.sk` :
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
import base64
|
|
242
|
+
from alea_q import mlkem_decaps
|
|
243
|
+
|
|
244
|
+
kp = client.platinum.slhdsa_keygen(recipient_pk=my_mlkem_pubkey_hex)
|
|
245
|
+
if kp.envelope:
|
|
246
|
+
# kp.sk est vide — la clé réelle est dans kp.envelope["encrypted_payload"],
|
|
247
|
+
# chiffrée pour mlkem_pubkey_hex. sk_bob_dk = dk ML-KEM de Bob (obtenu
|
|
248
|
+
# via son propre mlkem_keygen()) — la décapsulation reste locale, le
|
|
249
|
+
# serveur ne voit jamais ni sk_bob_dk ni le secret déchiffré.
|
|
250
|
+
#
|
|
251
|
+
# ATTENTION : les champs de l'enveloppe (ciphertext, encrypted_payload,
|
|
252
|
+
# nonce, signature, certropi_pk) sont en BASE64 — contrairement à
|
|
253
|
+
# dk/ek/shared_key (mlkem_keygen()/mlkem_encaps()) qui sont en HEX.
|
|
254
|
+
ciphertext = base64.b64decode(kp.envelope["ciphertext"])
|
|
255
|
+
shared_key = mlkem_decaps(sk_bob_dk, ciphertext, param_set="ML_KEM_768")
|
|
256
|
+
# shared_key (32 octets) est la clé AES-256-GCM utilisée pour chiffrer
|
|
257
|
+
# kp.envelope["encrypted_payload"] (nonce en base64 lui aussi) —
|
|
258
|
+
# déchiffrement AES-GCM restant à la charge de l'appelant, non fourni
|
|
259
|
+
# par ce SDK à ce jour.
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Gestion des erreurs
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from alea_q import AleaQClient
|
|
266
|
+
from alea_q.exceptions import BackendUnavailable, AuthenticationError, InsufficientBalance
|
|
267
|
+
import time
|
|
268
|
+
|
|
269
|
+
client = AleaQClient()
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
result = client.platinum.generate(n_bits=256)
|
|
273
|
+
except BackendUnavailable as e:
|
|
274
|
+
print(f"QPU indisponible — réessayer dans {e.retry_after}s")
|
|
275
|
+
time.sleep(e.retry_after or 300)
|
|
276
|
+
except AuthenticationError:
|
|
277
|
+
print("Clé API invalide")
|
|
278
|
+
except InsufficientBalance:
|
|
279
|
+
print("Solde prépayé épuisé (mode pay-as-you-go) — recharger via le support")
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Configuration
|
|
283
|
+
|
|
284
|
+
| Variable d'env | Description | Défaut |
|
|
285
|
+
|----------------------|------------------------------------------|-----------------------------|
|
|
286
|
+
| `ALEA_Q_API_KEY` | Clé API | — |
|
|
287
|
+
| `ALEA_Q_BASE_URL` | URL de base de l'API | `https://api.alea-q.com` |
|
|
288
|
+
| `ALEA_Q_TIMEOUT` | Timeout HTTP en secondes | `60` |
|
|
289
|
+
| `ALEA_Q_MAX_RETRIES` | Nombre de retries automatiques | `2` |
|
|
290
|
+
| `ALEA_Q_PUBKEY_PATH` | Chemin clé publique CERTROPI | `certropi_public.pem` |
|
|
291
|
+
|
|
292
|
+
`https://api.certropi.com` est un alias B2B de la même API — les deux
|
|
293
|
+
domaines répondent de manière identique, `ALEA_Q_BASE_URL` accepte
|
|
294
|
+
indifféremment l'un ou l'autre.
|
|
295
|
+
|
|
296
|
+
## Développement
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
pip install -e ".[dev]"
|
|
300
|
+
|
|
301
|
+
# Suite de tests (mock HTTP via respx — aucun accès réseau requis)
|
|
302
|
+
pytest
|
|
303
|
+
|
|
304
|
+
# Avec couverture
|
|
305
|
+
pytest --cov=alea_q --cov-report=term-missing
|
|
306
|
+
|
|
307
|
+
# Tests contre l'API réelle (marqués `smoke`, nécessitent ALEA_Q_API_KEY
|
|
308
|
+
# et un accès réseau — exclus par défaut)
|
|
309
|
+
pytest -m smoke
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Licence
|
|
313
|
+
|
|
314
|
+
Propriétaire — CERTROPI © 2026
|
|
315
|
+
|