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.
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ dist/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .DS_Store
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
+