Signet Forge 0.1.0
C++20 Parquet library with AI-native extensions
DEMO
Loading...
Searching...
No Matches
post_quantum.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright 2026 Johnson Ogundeji
3#pragma once
4
8
9// ---------------------------------------------------------------------------
10// post_quantum.hpp -- Post-quantum cryptography for SignetStack Signet Forge
11//
12// Provides two NIST post-quantum cryptographic primitives:
13//
14// Kyber-768 KEM -- Key Encapsulation Mechanism for establishing shared
15// AES-256 keys between Parquet writer and reader.
16// (NIST FIPS 203 / ML-KEM-768)
17//
18// Dilithium-3 -- Digital signature scheme for Parquet footer signing
19// and tamper detection.
20// (NIST FIPS 204 / ML-DSA-65)
21//
22// Additionally provides a hybrid KEM (Kyber-768 + X25519) that combines
23// post-quantum and classical key exchange for defense-in-depth.
24//
25// This is the first Parquet library to offer post-quantum encryption.
26//
27// TWO MODES OF OPERATION:
28//
29// 1. Bundled mode (default):
30// A simplified reference implementation using std::random_device and
31// SHA-256 for key generation, encapsulation stubs, and signature
32// simulation. This mode demonstrates the API and allows integration
33// testing WITHOUT any external dependencies.
34//
35// *** WARNING: The bundled mode is NOT cryptographically secure. ***
36// *** It does NOT implement real Kyber or Dilithium algorithms. ***
37// *** It is a functional stub for API development and testing. ***
38//
39// 2. liboqs mode (SIGNET_HAS_LIBOQS defined):
40// Delegates to the Open Quantum Safe (liboqs) library for real
41// NIST-standardized Kyber-768 and Dilithium-3 implementations.
42// This is the recommended mode for production use.
43//
44// Header-only, zero external dependencies in bundled mode.
45//
46// References:
47// - NIST FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism
48// - NIST FIPS 204: Module-Lattice-Based Digital Signature Standard
49// - https://openquantumsafe.org/
50// - https://pq-crystals.org/kyber/
51// - https://pq-crystals.org/dilithium/
52// ---------------------------------------------------------------------------
53
54#include "signet/error.hpp"
56
57#if !defined(SIGNET_ENABLE_COMMERCIAL) || !SIGNET_ENABLE_COMMERCIAL
58#error "signet/crypto/post_quantum.hpp requires SIGNET_ENABLE_COMMERCIAL=ON (AGPL-3.0 commercial tier). See LICENSE_COMMERCIAL."
59#endif
60
61#if defined(SIGNET_REQUIRE_REAL_PQ) && SIGNET_REQUIRE_REAL_PQ && !defined(SIGNET_HAS_LIBOQS)
62#error "SIGNET_REQUIRE_REAL_PQ=1 forbids bundled PQ stubs. Reconfigure with -DSIGNET_ENABLE_PQ=ON and install liboqs."
63#endif
64
65#if !defined(SIGNET_HAS_LIBOQS)
66#pragma message("WARNING: Signet post-quantum crypto is using BUNDLED Kyber/Dilithium STUBS (NOT post-quantum secure). HybridKem X25519 provides real classical ECDH security; only the Kyber-768 lattice portion is a structural placeholder. Build with -DSIGNET_ENABLE_PQ=ON for real post-quantum resistance.")
67#endif
68
69#include <algorithm>
70#include <array>
71#include <cstddef>
72#include <cstdint>
73#include <cstring>
74#include <random>
75#include <string>
76#include <utility>
77#include <vector>
78
79#ifdef SIGNET_HAS_LIBOQS
80#include <oqs/oqs.h>
81#endif
82
83// SHA-256 is now in signet/crypto/sha256.hpp (Tier 1, AGPL-3.0-or-later).
85
86namespace signet::forge::crypto {
87
93[[nodiscard]] inline bool is_real_pq_crypto() noexcept {
94#ifdef SIGNET_HAS_LIBOQS
95 return true;
96#else
97 return false;
98#endif
99}
100
101#ifdef SIGNET_HAS_LIBOQS
102#if defined(OQS_KEM_alg_kyber_768)
103inline constexpr const char* kOqsKemAlgMlKem768 = OQS_KEM_alg_kyber_768;
104#elif defined(OQS_KEM_alg_ml_kem_768)
105inline constexpr const char* kOqsKemAlgMlKem768 = OQS_KEM_alg_ml_kem_768;
106#else
107#error "liboqs is missing ML-KEM-768/Kyber-768 symbols required by SIGNET_REQUIRE_REAL_PQ"
108#endif
109
110#if defined(OQS_SIG_alg_dilithium_3)
111inline constexpr const char* kOqsSigAlgMlDsa65 = OQS_SIG_alg_dilithium_3;
112#elif defined(OQS_SIG_alg_ml_dsa_65)
113inline constexpr const char* kOqsSigAlgMlDsa65 = OQS_SIG_alg_ml_dsa_65;
114#else
115#error "liboqs is missing ML-DSA-65/Dilithium-3 symbols required by SIGNET_REQUIRE_REAL_PQ"
116#endif
117#endif
118
119// ===========================================================================
120// Random byte generation helper (used by all PQ classes)
121// ===========================================================================
122namespace detail::pq {
123
128inline void random_bytes(uint8_t* buf, size_t size) {
130}
131
133inline std::vector<uint8_t> random_bytes(size_t size) {
134 std::vector<uint8_t> buf(size);
135 random_bytes(buf.data(), size);
136 return buf;
137}
138
139} // namespace detail::pq
140
141// ===========================================================================
142// detail::x25519 — Constant-time X25519 (RFC 7748 Curve25519)
143//
144// Field: GF(2^255 - 19).
145// Representation: 5-limb radix-2^51 (Clang/GCC) or 10-limb radix-2^25.5 (MSVC).
146// All operations are constant-time (no data-dependent branches, no timing leaks).
147//
148// References:
149// - RFC 7748: Elliptic Curves for Diffie-Hellman Key Agreement
150// - D.J. Bernstein "Curve25519: new Diffie-Hellman speed records" (2006)
151// - SUPERCOP ref10 implementation
152// ===========================================================================
153namespace detail::x25519 {
154
155// ---------------------------------------------------------------------------
156// Field arithmetic — GF(2^255 - 19)
157//
158// We use two representations depending on compiler:
159// GCC/Clang: 5 x uint64_t limbs, radix 2^51. Products use unsigned __int128.
160// MSVC: 10 x int32_t limbs, radix 2^25.5. Products use int64_t.
161// ---------------------------------------------------------------------------
162
163#if defined(__GNUC__) || defined(__clang__)
164
165// --- 5-limb 64-bit representation (Clang / GCC) ---
166
167using u128 = unsigned __int128;
168
171using Fe = std::array<uint64_t, 5>;
172
174inline Fe fe_carry(Fe a) {
175 constexpr uint64_t MASK51 = (1ULL << 51) - 1;
176 for (int pass = 0; pass < 2; ++pass) {
177 uint64_t c;
178 c = a[0] >> 51; a[0] &= MASK51; a[1] += c;
179 c = a[1] >> 51; a[1] &= MASK51; a[2] += c;
180 c = a[2] >> 51; a[2] &= MASK51; a[3] += c;
181 c = a[3] >> 51; a[3] &= MASK51; a[4] += c;
182 c = a[4] >> 51; a[4] &= MASK51; a[0] += c * 19;
183 }
184 return a;
185}
186
187inline Fe fe_add(Fe a, const Fe& b) {
188 for (int i = 0; i < 5; ++i) a[i] += b[i];
189 return fe_carry(a);
190}
191
192inline Fe fe_sub(Fe a, const Fe& b) {
193 // Add 2p before subtracting to stay positive (RFC 7748 §5, p = 2^255-19)
194 a[0] += 0xFFFFFFFFFFFDAULL - b[0]; // 2p[0] = 2*(2^51-19) — absorbs p's -19 term
195 a[1] += 0xFFFFFFFFFFFFEULL - b[1];
196 a[2] += 0xFFFFFFFFFFFFEULL - b[2];
197 a[3] += 0xFFFFFFFFFFFFEULL - b[3];
198 a[4] += 0xFFFFFFFFFFFFEULL - b[4];
199 return fe_carry(a);
200}
201
202inline Fe fe_mul(const Fe& a, const Fe& b) {
203 // Schoolbook multiplication, reduced mod p on-the-fly.
204 // 19*b[i] precomputed to avoid repeated multiplications.
205 u128 t0 = (u128)a[0]*b[0] + 19*((u128)a[1]*b[4] + (u128)a[2]*b[3] + (u128)a[3]*b[2] + (u128)a[4]*b[1]);
206 u128 t1 = (u128)a[0]*b[1] + (u128)a[1]*b[0] + 19*((u128)a[2]*b[4] + (u128)a[3]*b[3] + (u128)a[4]*b[2]);
207 u128 t2 = (u128)a[0]*b[2] + (u128)a[1]*b[1] + (u128)a[2]*b[0] + 19*((u128)a[3]*b[4] + (u128)a[4]*b[3]);
208 u128 t3 = (u128)a[0]*b[3] + (u128)a[1]*b[2] + (u128)a[2]*b[1] + (u128)a[3]*b[0] + 19*(u128)a[4]*b[4];
209 u128 t4 = (u128)a[0]*b[4] + (u128)a[1]*b[3] + (u128)a[2]*b[2] + (u128)a[3]*b[1] + (u128)a[4]*b[0];
210
211 constexpr uint64_t MASK51 = (1ULL << 51) - 1;
212 Fe r;
213 uint64_t c;
214 r[0] = (uint64_t)t0 & MASK51; c = (uint64_t)(t0 >> 51); t1 += c;
215 r[1] = (uint64_t)t1 & MASK51; c = (uint64_t)(t1 >> 51); t2 += c;
216 r[2] = (uint64_t)t2 & MASK51; c = (uint64_t)(t2 >> 51); t3 += c;
217 r[3] = (uint64_t)t3 & MASK51; c = (uint64_t)(t3 >> 51); t4 += c;
218 r[4] = (uint64_t)t4 & MASK51; c = (uint64_t)(t4 >> 51);
219 r[0] += c * 19;
220 c = r[0] >> 51; r[0] &= MASK51; r[1] += c;
221 return fe_carry(r);
222}
223
224inline Fe fe_sq(const Fe& a) { return fe_mul(a, a); }
225
227inline Fe fe_from_bytes(const uint8_t* b) {
228 constexpr uint64_t MASK51 = (1ULL << 51) - 1;
229 auto load8 = [&](int i) -> uint64_t {
230 uint64_t v = 0;
231 for (int j = 0; j < 8 && i+j < 32; ++j)
232 v |= (uint64_t)b[i+j] << (8*j);
233 return v;
234 };
235 Fe f;
236 f[0] = load8( 0) & MASK51;
237 f[1] = (load8( 6) >> 3) & MASK51;
238 f[2] = (load8(12) >> 6) & MASK51;
239 f[3] = (load8(19) >> 1) & MASK51;
240 f[4] = (load8(24) >> 12) & MASK51;
241 return fe_carry(f);
242}
243
245inline void fe_to_bytes(uint8_t* out, Fe f) {
246 constexpr uint64_t MASK51 = (1ULL << 51) - 1;
247
248 // Fully normalize carries before canonical reduction.
249 f = fe_carry(f);
250 f = fe_carry(f);
251
252 // Compute g = f - p by adding 19 and carrying into bit 255.
253 uint64_t g0 = f[0] + 19;
254 uint64_t c = g0 >> 51; g0 &= MASK51;
255 uint64_t g1 = f[1] + c; c = g1 >> 51; g1 &= MASK51;
256 uint64_t g2 = f[2] + c; c = g2 >> 51; g2 &= MASK51;
257 uint64_t g3 = f[3] + c; c = g3 >> 51; g3 &= MASK51;
258 uint64_t g4 = f[4] + c; c = g4 >> 51; g4 &= MASK51;
259
260 // If carry==1, f >= p and g is the reduced canonical representative.
261 uint64_t mask = 0 - c;
262 f[0] ^= mask & (f[0] ^ g0);
263 f[1] ^= mask & (f[1] ^ g1);
264 f[2] ^= mask & (f[2] ^ g2);
265 f[3] ^= mask & (f[3] ^ g3);
266 f[4] ^= mask & (f[4] ^ g4);
267
268 // Pack radix-2^51 limbs into 32 little-endian bytes without overflow.
269 const uint64_t w0 = f[0] | ((f[1] & ((1ULL << 13) - 1)) << 51);
270 const uint64_t w1 = (f[1] >> 13) | ((f[2] & ((1ULL << 26) - 1)) << 38);
271 const uint64_t w2 = (f[2] >> 26) | ((f[3] & ((1ULL << 39) - 1)) << 25);
272 const uint64_t w3 = (f[3] >> 39) | (f[4] << 12);
273
274 auto store8 = [&](uint8_t* p, uint64_t v) {
275 for (int i = 0; i < 8; ++i) p[i] = static_cast<uint8_t>(v >> (8 * i));
276 };
277 store8(out + 0, w0);
278 store8(out + 8, w1);
279 store8(out + 16, w2);
280 store8(out + 24, w3);
281}
282
285inline void fe_cswap(Fe& a, Fe& b, uint64_t swap) {
286 uint64_t mask = 0 - swap; // 0 if swap==0, 0xFFFF...FF if swap==1
287 for (int i = 0; i < 5; ++i) {
288 uint64_t t = mask & (a[i] ^ b[i]);
289 a[i] ^= t;
290 b[i] ^= t;
291 }
292}
293
297inline Fe fe_inv(const Fe& z) {
298 // Constant-time addition chain for z^(p-2) mod p, p = 2^255 - 19
299 // Follows NaCl ref10 / SUPERCOP — branch-free (CWE-208 compliant)
300 Fe t0 = fe_sq(z); // z^2
301 Fe t1 = fe_sq(fe_sq(t0)); // z^8
302 t1 = fe_mul(t1, z); // z^9
303 t0 = fe_mul(t0, t1); // z^11
304 Fe t2 = fe_sq(t0); // z^22
305 t2 = fe_mul(t2, t1); // z^(2^5-1) = z^31
306 // z^(2^10-1)
307 Fe a = t2; for (int i = 0; i < 5; ++i) a = fe_sq(a);
308 a = fe_mul(a, t2);
309 // z^(2^20-1)
310 Fe b = a; for (int i = 0; i < 10; ++i) b = fe_sq(b);
311 b = fe_mul(b, a);
312 // z^(2^40-1)
313 Fe c = b; for (int i = 0; i < 20; ++i) c = fe_sq(c);
314 c = fe_mul(c, b);
315 // z^(2^50-1)
316 for (int i = 0; i < 10; ++i) c = fe_sq(c);
317 c = fe_mul(c, a);
318 // z^(2^100-1)
319 Fe d = c; for (int i = 0; i < 50; ++i) d = fe_sq(d);
320 d = fe_mul(d, c);
321 // z^(2^200-1)
322 Fe e = d; for (int i = 0; i < 100; ++i) e = fe_sq(e);
323 e = fe_mul(e, d);
324 // z^(2^250-1)
325 for (int i = 0; i < 50; ++i) e = fe_sq(e);
326 e = fe_mul(e, c);
327 // z^(2^255-32) after squaring, then * z^11 = z^(2^255-21) = z^(p-2)
328 for (int i = 0; i < 5; ++i) e = fe_sq(e);
329 return fe_mul(e, t0);
330}
331
332#else // MSVC / other compilers — 10-limb int32_t representation
333
335using Fe = std::array<int32_t, 10>;
336
338inline Fe fe_from_bytes(const uint8_t* b) {
339 auto load4 = [&](int i) -> int32_t {
340 return (int32_t)b[i] | ((int32_t)b[i+1]<<8) | ((int32_t)b[i+2]<<16) | ((int32_t)b[i+3]<<24);
341 };
342 Fe h;
343 h[0] = load4( 0) & 0x3FFFFFF;
344 h[1] = (load4( 3) >> 2) & 0x1FFFFFF;
345 h[2] = (load4( 6) >> 3) & 0x3FFFFFF;
346 h[3] = (load4( 9) >> 5) & 0x1FFFFFF;
347 h[4] = (load4(12) >> 6) & 0x3FFFFFF;
348 h[5] = load4(16) & 0x1FFFFFF;
349 h[6] = (load4(19) >> 1) & 0x3FFFFFF;
350 h[7] = (load4(22) >> 3) & 0x1FFFFFF;
351 h[8] = (load4(25) >> 4) & 0x3FFFFFF;
352 h[9] = (load4(28) >> 6) & 0x1FFFFFF;
353 return h;
354}
355
356inline void fe_carry_10(Fe& h) {
357 for (int i = 0; i < 10; ++i) {
358 int bits = (i % 2 == 0) ? 26 : 25;
359 int32_t carry = h[i] >> bits;
360 h[i] -= carry << bits;
361 if (i < 9) h[i+1] += carry;
362 else h[0] += carry * 19;
363 }
364}
365
366inline Fe fe_add(Fe a, const Fe& b) {
367 for (int i = 0; i < 10; ++i) a[i] += b[i];
368 return a;
369}
370
371inline Fe fe_sub(Fe a, const Fe& b) {
372 // Add 2p in each limb to stay non-negative.
373 // RFC 7748 §5: p = 2^255 - 19. Limb 0 absorbs the -19 constant.
374 // Audit #6 fix: corrected 2p[0] from 0x3FFFFF0 to 2*(2^26-19) — the
375 // prior value silently corrupted all X25519 on MSVC (CWE-682).
376 int32_t two_p[10] = {
377 2 * (0x3FFFFFF - 18), // 2*(2^26 - 19) — absorbs p's -19 term
378 2 * 0x1FFFFFF, // 2*(2^25 - 1)
379 2 * 0x3FFFFFF, // 2*(2^26 - 1)
380 2 * 0x1FFFFFF, // 2*(2^25 - 1)
381 2 * 0x3FFFFFF, // 2*(2^26 - 1)
382 2 * 0x1FFFFFF, // 2*(2^25 - 1)
383 2 * 0x3FFFFFF, // 2*(2^26 - 1)
384 2 * 0x1FFFFFF, // 2*(2^25 - 1)
385 2 * 0x3FFFFFF, // 2*(2^26 - 1)
386 2 * 0x1FFFFFF // 2*(2^25 - 1)
387 };
388 for (int i = 0; i < 10; ++i) a[i] = a[i] + two_p[i] - b[i];
389 fe_carry_10(a);
390 return a;
391}
392
393inline Fe fe_mul(const Fe& f, const Fe& g) {
394 // 10x10 schoolbook with cross-product combining. Each product is int64_t.
395 // KNOWN ISSUE (H-2, MSVC 10-limb path): The truncation from int64_t to
396 // int32_t below loses high bits before carry propagation. This is
397 // functionally correct only when each h[i] fits in int32_t after the
398 // double fe_carry_10 pass, which holds for field elements in reduced
399 // form. A future MSVC-validated fix should accumulate in int64_t and
400 // carry in 64-bit precision (matching the ref10 implementation).
401 int64_t h[10] = {};
402 // 19*g[i] for i>=5 collapses the wrap-around
403 int64_t g19[5];
404 for (int i = 0; i < 5; ++i) g19[i] = (int64_t)g[i+5] * 19;
405 for (int i = 0; i < 5; ++i) {
406 for (int j = 0; j < 5; ++j) {
407 h[i+j] += (int64_t)f[i] * g[j];
408 h[i+j+5] += (int64_t)f[i] * g[j+5];
409 h[i+j] += (int64_t)f[i+5] * g19[j]; // wrap
410 }
411 }
412 Fe r;
413 for (int i = 0; i < 10; ++i) r[i] = (int32_t)h[i];
415 return r;
416}
417
418inline Fe fe_sq(const Fe& a) { return fe_mul(a, a); }
419
420inline void fe_to_bytes(uint8_t* out, Fe h) {
421 // KNOWN ISSUE (H-3, MSVC 10-limb path): The conditional subtraction
422 // and packing below use data-dependent branching on limb values,
423 // which is not constant-time on MSVC (no __int128 path). This is
424 // acceptable for X25519 public key operations but must be addressed
425 // before using this path for secret-dependent scalar operations on
426 // MSVC. Tracked for fix when MSVC support is fully validated.
428 // Conditionally subtract p
429 int32_t q = (19 * h[0] + (1<<25)) >> 26;
430 for (int i = 0; i < 9; ++i) {
431 q = h[i] + q * (i%2==0 ? 19 : 1);
432 q >>= (i%2==0 ? 26 : 25);
433 }
434 h[0] += 19 * q;
435 fe_carry_10(h);
436 // Pack
437 uint32_t b[8];
438 b[0] = (uint32_t)h[0] | ((uint32_t)h[1]<<26);
439 b[1] = ((uint32_t)h[1]>>6) | ((uint32_t)h[2]<<19);
440 b[2] = ((uint32_t)h[2]>>13) | ((uint32_t)h[3]<<13);
441 b[3] = ((uint32_t)h[3]>>19) | ((uint32_t)h[4]<<6);
442 b[4] = (uint32_t)h[5] | ((uint32_t)h[6]<<25);
443 b[5] = ((uint32_t)h[6]>>7) | ((uint32_t)h[7]<<18);
444 b[6] = ((uint32_t)h[7]>>14) | ((uint32_t)h[8]<<11);
445 b[7] = ((uint32_t)h[8]>>21) | ((uint32_t)h[9]<<4);
446 for (int i = 0; i < 8; ++i) {
447 out[i*4+0] = (uint8_t)(b[i]);
448 out[i*4+1] = (uint8_t)(b[i]>>8);
449 out[i*4+2] = (uint8_t)(b[i]>>16);
450 out[i*4+3] = (uint8_t)(b[i]>>24);
451 }
452}
453
454inline void fe_cswap(Fe& a, Fe& b, uint64_t swap) {
455 int32_t mask = (int32_t)(0 - swap);
456 for (int i = 0; i < 10; ++i) {
457 int32_t t = mask & (a[i] ^ b[i]);
458 a[i] ^= t; b[i] ^= t;
459 }
460}
461
462inline Fe fe_inv(const Fe& z) {
463 // Same addition chain (ref10), but using 10-limb operations
464 Fe t0 = fe_sq(z); // z^2
465 Fe t1 = fe_sq(fe_sq(t0)); // z^8
466 t1 = fe_mul(t1, z); // z^9
467 t0 = fe_mul(t0, t1); // z^11
468 Fe t2 = fe_sq(t0); // z^22
469 t2 = fe_mul(t2, t1); // z^31 = z^(2^5-1)
470 Fe a = t2;
471 for (int i=0;i<5;i++) a=fe_sq(a);
472 a=fe_mul(a,t2);
473 Fe b=a; for(int i=0;i<10;i++) b=fe_sq(b); b=fe_mul(b,a);
474 Fe c=b; for(int i=0;i<20;i++) c=fe_sq(c); c=fe_mul(c,b);
475 for(int i=0;i<10;i++) c=fe_sq(c); c=fe_mul(c,a);
476 Fe d=c; for(int i=0;i<50;i++) d=fe_sq(d); d=fe_mul(d,c);
477 Fe e=d; for(int i=0;i<100;i++) e=fe_sq(e); e=fe_mul(e,d);
478 for(int i=0;i<50;i++) e=fe_sq(e); e=fe_mul(e,c);
479 for(int i=0;i<5;i++) e=fe_sq(e);
480 return fe_mul(e,t0); // z^(2^255-32) * z^11 = z^(2^255-21) = z^(p-2)
481}
482
483#endif // __GNUC__ || __clang__
484
485// ---------------------------------------------------------------------------
486// X25519 Montgomery ladder (common to both representations)
487// ---------------------------------------------------------------------------
488
491inline std::array<uint8_t, 32> clamp_scalar(std::array<uint8_t, 32> k) {
492 k[0] &= 248;
493 k[31] &= 127;
494 k[31] |= 64;
495 return k;
496}
497
501inline std::array<uint8_t, 32> x25519_raw(
502 const std::array<uint8_t, 32>& scalar,
503 const std::array<uint8_t, 32>& point_u)
504{
505 // Decode input u-coordinate, masking the high bit per RFC 7748 §5
506 std::array<uint8_t, 32> u_masked = point_u;
507 u_masked[31] &= 0x7F; // Ignore the top bit
508 Fe u = fe_from_bytes(u_masked.data());
509
510 // Montgomery ladder state: (x2,z2)=1 (point at infinity), (x3,z3)=u
511 Fe x_1 = u;
512 Fe x_2;
513#if defined(__GNUC__) || defined(__clang__)
514 x_2 = {1,0,0,0,0};
515#else
516 x_2 = {1,0,0,0,0,0,0,0,0,0};
517#endif
518 Fe z_2;
519#if defined(__GNUC__) || defined(__clang__)
520 z_2 = {0,0,0,0,0};
521#else
522 z_2 = {0,0,0,0,0,0,0,0,0,0};
523#endif
524 Fe x_3 = u;
525 Fe z_3;
526#if defined(__GNUC__) || defined(__clang__)
527 z_3 = {1,0,0,0,0};
528#else
529 z_3 = {1,0,0,0,0,0,0,0,0,0};
530#endif
531
532 // a24 as field element (121665)
533 Fe a24;
534#if defined(__GNUC__) || defined(__clang__)
535 a24 = {121665, 0, 0, 0, 0};
536#else
537 a24 = {121665, 0, 0, 0, 0, 0, 0, 0, 0, 0};
538#endif
539
540 uint64_t swap = 0;
541
542 // 255-bit scalar, iterate from bit 254 down to 0
543 for (int i = 254; i >= 0; --i) {
544 uint64_t k_bit = (scalar[i / 8] >> (i % 8)) & 1;
545 swap ^= k_bit;
546 fe_cswap(x_2, x_3, swap);
547 fe_cswap(z_2, z_3, swap);
548 swap = k_bit;
549
550 // Montgomery differential addition-and-doubling
551 Fe A = fe_add(x_2, z_2);
552 Fe AA = fe_sq(A);
553 Fe B = fe_sub(x_2, z_2);
554 Fe BB = fe_sq(B);
555 Fe E = fe_sub(AA, BB);
556 Fe C = fe_add(x_3, z_3);
557 Fe D = fe_sub(x_3, z_3);
558 Fe DA = fe_mul(D, A);
559 Fe CB = fe_mul(C, B);
560 Fe t1 = fe_add(DA, CB);
561 Fe t2 = fe_sub(DA, CB);
562 x_3 = fe_sq(t1);
563 z_3 = fe_mul(fe_sq(t2), x_1);
564 x_2 = fe_mul(AA, BB);
565 z_2 = fe_mul(E, fe_add(AA, fe_mul(a24, E)));
566 }
567
568 // Final conditional swap
569 fe_cswap(x_2, x_3, swap);
570 fe_cswap(z_2, z_3, swap);
571
572 // Recover u-coordinate: x_2 * z_2^(-1)
573 Fe result = fe_mul(x_2, fe_inv(z_2));
574
575 std::array<uint8_t, 32> out;
576 fe_to_bytes(out.data(), result);
577 return out;
578}
579
584 const std::array<uint8_t, 32>& scalar,
585 const std::array<uint8_t, 32>& u_coord)
586{
587 auto license = commercial::require_feature("PQ x25519");
588 if (!license) return license.error();
589
590 // L-C8: Clamp scalar per RFC 7748 §5 before use, in case caller
591 // passes an unclamped key. This is idempotent if already clamped.
592 std::array<uint8_t, 32> clamped = scalar;
593 clamped[0] &= 248;
594 clamped[31] &= 127;
595 clamped[31] |= 64;
596
597 auto result = x25519_raw(clamped, u_coord);
598 // Constant-time zero check: OR all 32 bytes, then compare (RFC 7748 §6.1, CWE-208)
599 uint8_t acc = 0;
600 for (size_t i = 0; i < 32; ++i) acc |= result[i];
601 // acc == 0 means degenerate key (all-zero output)
602 if (acc == 0) {
604 "X25519: degenerate output (all-zero) — invalid input point"};
605 }
606 return result;
607}
608
610inline const std::array<uint8_t, 32>& base_point() {
611 static const std::array<uint8_t, 32> BP = {
612 9,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
613 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0
614 };
615 return BP;
616}
617
621inline expected<std::pair<std::array<uint8_t,32>, std::array<uint8_t,32>>>
623 auto license = commercial::require_feature("PQ x25519 keypair");
624 if (!license) return license.error();
625
626 std::array<uint8_t,32> sk;
627 pq::random_bytes(sk.data(), 32);
628 sk = clamp_scalar(sk);
629 auto pk_result = x25519(sk, base_point());
630 if (!pk_result) return pk_result.error();
631 return std::make_pair(sk, *pk_result);
632}
633
634} // namespace detail::x25519
635
636// ===========================================================================
637// KyberKem -- Kyber-768 Key Encapsulation Mechanism
638//
639// Used to establish shared AES-256 keys between Parquet writer and reader.
640//
641// Kyber-768 is the NIST ML-KEM-768 standard (FIPS 203), providing
642// approximately 192-bit post-quantum security.
643//
644// Parameter set (Kyber-768 / ML-KEM-768):
645// Public key: 1184 bytes
646// Secret key: 2400 bytes
647// Ciphertext: 1088 bytes
648// Shared secret: 32 bytes (256 bits, suitable for AES-256)
649// ===========================================================================
650
663class KyberKem {
664public:
665#if defined(SIGNET_HAS_LIBOQS) && defined(OQS_KEM_ml_kem_768_length_public_key)
666 static constexpr size_t PUBLIC_KEY_SIZE = OQS_KEM_ml_kem_768_length_public_key;
667 static constexpr size_t SECRET_KEY_SIZE = OQS_KEM_ml_kem_768_length_secret_key;
668 static constexpr size_t CIPHERTEXT_SIZE = OQS_KEM_ml_kem_768_length_ciphertext;
669 static constexpr size_t SHARED_SECRET_SIZE = OQS_KEM_ml_kem_768_length_shared_secret;
670#elif defined(SIGNET_HAS_LIBOQS) && defined(OQS_KEM_kyber_768_length_public_key)
671 static constexpr size_t PUBLIC_KEY_SIZE = OQS_KEM_kyber_768_length_public_key;
672 static constexpr size_t SECRET_KEY_SIZE = OQS_KEM_kyber_768_length_secret_key;
673 static constexpr size_t CIPHERTEXT_SIZE = OQS_KEM_kyber_768_length_ciphertext;
674 static constexpr size_t SHARED_SECRET_SIZE = OQS_KEM_kyber_768_length_shared_secret;
675#else
676 static constexpr size_t PUBLIC_KEY_SIZE = 1184;
677 static constexpr size_t SECRET_KEY_SIZE = 2400;
678 static constexpr size_t CIPHERTEXT_SIZE = 1088;
679 static constexpr size_t SHARED_SECRET_SIZE = 32;
680#endif
681
683 struct KeyPair {
684 std::vector<uint8_t> public_key;
685 std::vector<uint8_t> secret_key;
686
689 if (!secret_key.empty()) {
690 volatile uint8_t* p = secret_key.data();
691 for (size_t i = 0; i < secret_key.size(); ++i) p[i] = 0;
692 }
693 }
694 };
695
698 std::vector<uint8_t> ciphertext;
699 std::vector<uint8_t> shared_secret;
700 };
701
702 // -----------------------------------------------------------------------
703 // generate_keypair -- Generate a Kyber-768 keypair
704 //
705 // Bundled mode:
706 // Generates random bytes for public/secret keys. The keys are
707 // structurally valid (correct sizes) but NOT real Kyber lattice keys.
708 //
709 // liboqs mode:
710 // Delegates to OQS_KEM_kyber_768_keypair() for real key generation.
711 // -----------------------------------------------------------------------
715 [[nodiscard]] static expected<KeyPair> generate_keypair() {
716 auto license = commercial::require_feature("Kyber generate_keypair");
717 if (!license) return license.error();
718
719#ifdef SIGNET_HAS_LIBOQS
720 // --- liboqs mode: real Kyber-768 key generation ---
721 OQS_KEM* kem = OQS_KEM_new(kOqsKemAlgMlKem768);
722 if (!kem) {
724 "KyberKem: failed to initialize OQS Kyber-768"};
725 }
726
727 KeyPair kp;
728 kp.public_key.resize(kem->length_public_key);
729 kp.secret_key.resize(kem->length_secret_key);
730
731 OQS_STATUS rc = OQS_KEM_keypair(kem,
732 kp.public_key.data(),
733 kp.secret_key.data());
734 OQS_KEM_free(kem);
735
736 if (rc != OQS_SUCCESS) {
738 "KyberKem: OQS keypair generation failed"};
739 }
740
741 return kp;
742#else
743 // --- REFERENCE IMPLEMENTATION — use liboqs for production ---
744 //
745 // Generates random bytes of the correct sizes. These are NOT
746 // real Kyber lattice keys and provide NO post-quantum security.
747 // The encapsulate/decapsulate functions below use SHA-256-based
748 // deterministic derivation so that the round-trip is functional.
749 KeyPair kp;
752
753 // Embed a copy of the public key hash at the end of the secret key
754 // so decapsulation can derive the same shared secret.
755 // secret_key layout: [random_seed(2368)] [pk_hash(32)]
756 auto pk_hash = detail::sha256::sha256(kp.public_key);
757 std::memcpy(kp.secret_key.data() + SECRET_KEY_SIZE - 32,
758 pk_hash.data(), 32);
759
760 return kp;
761#endif
762 }
763
764 // -----------------------------------------------------------------------
765 // encapsulate -- Generate a shared secret from a public key
766 //
767 // The sender calls this with the recipient's public key. It produces:
768 // - ciphertext: sent to the recipient (1088 bytes)
769 // - shared_secret: used locally as the AES-256 key (32 bytes)
770 //
771 // Bundled mode:
772 // Generates a random seed, derives shared_secret = SHA-256(seed || pk),
773 // and produces ciphertext = seed XOR SHA-256(pk). This is NOT real
774 // Kyber encapsulation but demonstrates the API contract.
775 //
776 // liboqs mode:
777 // Delegates to OQS_KEM_kyber_768_encaps().
778 // -----------------------------------------------------------------------
786 const uint8_t* public_key, size_t pk_size) {
787
788 auto license = commercial::require_feature("Kyber encapsulate");
789 if (!license) return license.error();
790
791 if (pk_size != PUBLIC_KEY_SIZE) {
793 "KyberKem: public key must be "
794 + std::to_string(PUBLIC_KEY_SIZE) + " bytes, got "
795 + std::to_string(pk_size)};
796 }
797
798#ifdef SIGNET_HAS_LIBOQS
799 // --- liboqs mode: real Kyber-768 encapsulation ---
800 OQS_KEM* kem = OQS_KEM_new(kOqsKemAlgMlKem768);
801 if (!kem) {
803 "KyberKem: failed to initialize OQS Kyber-768"};
804 }
805
806 EncapsulationResult result;
807 result.ciphertext.resize(kem->length_ciphertext);
808 result.shared_secret.resize(kem->length_shared_secret);
809
810 OQS_STATUS rc = OQS_KEM_encaps(kem,
811 result.ciphertext.data(),
812 result.shared_secret.data(),
813 public_key);
814 OQS_KEM_free(kem);
815
816 if (rc != OQS_SUCCESS) {
818 "KyberKem: OQS encapsulation failed"};
819 }
820
821 return result;
822#else
823 // --- REFERENCE IMPLEMENTATION — use liboqs for production ---
824 //
825 // 1. Generate random 32-byte seed
826 // 2. shared_secret = SHA-256(seed || public_key_hash)
827 // 3. ciphertext = [seed(32)] [pk_hash_xor_pad(1056)] -- padded to 1088
828 //
829 // The ciphertext contains the seed in cleartext (XORed with pk-derived
830 // mask for minimal obfuscation). This is NOT cryptographically secure.
831
832 // CR-2: Unless SIGNET_ALLOW_STUB_PQ is explicitly defined, refuse to
833 // perform actual encryption with the insecure stub implementation.
834#ifndef SIGNET_ALLOW_STUB_PQ
836 "KyberKem: stub PQ implementation provides zero security. "
837 "Build with -DSIGNET_ENABLE_PQ=ON (liboqs) for real Kyber-768, "
838 "or define SIGNET_ALLOW_STUB_PQ to allow the insecure stub."};
839#endif
840
841 EncapsulationResult result;
842
843 // Generate random seed
844 std::array<uint8_t, 32> seed;
845 detail::pq::random_bytes(seed.data(), 32);
846
847 // Derive public key hash
848 auto pk_hash = detail::sha256::sha256(public_key, pk_size);
849
850 // Derive shared secret: SHA-256(seed || pk_hash)
851 result.shared_secret.resize(SHARED_SECRET_SIZE);
853 seed.data(), seed.size(), pk_hash.data(), pk_hash.size());
854 std::memcpy(result.shared_secret.data(), ss.data(), SHARED_SECRET_SIZE);
855
856 // Build ciphertext (1088 bytes):
857 // [seed XOR pk_hash (32 bytes)] [padding derived from pk_hash (1056 bytes)]
858 result.ciphertext.resize(CIPHERTEXT_SIZE, 0);
859
860 // XOR seed with pk_hash for minimal obfuscation
861 for (size_t i = 0; i < 32; ++i) {
862 result.ciphertext[i] = seed[i] ^ pk_hash[i];
863 }
864
865 // Fill remaining ciphertext with deterministic padding from pk_hash
866 // so that the decapsulator can identify and strip it
867 for (size_t i = 32; i < CIPHERTEXT_SIZE; ++i) {
868 result.ciphertext[i] = pk_hash[i % 32] ^ static_cast<uint8_t>(i);
869 }
870
871 return result;
872#endif
873 }
874
875 // -----------------------------------------------------------------------
876 // decapsulate -- Recover the shared secret from ciphertext + secret key
877 //
878 // The recipient calls this with the received ciphertext and their
879 // secret key. It produces the same 32-byte shared secret that the
880 // sender derived during encapsulation.
881 //
882 // Bundled mode:
883 // Extracts the seed from ciphertext by XORing with pk_hash (derived
884 // from the pk_hash embedded in the secret key), then derives
885 // shared_secret = SHA-256(seed || pk_hash).
886 //
887 // liboqs mode:
888 // Delegates to OQS_KEM_kyber_768_decaps().
889 // -----------------------------------------------------------------------
899 const uint8_t* ciphertext, size_t ct_size,
900 const uint8_t* secret_key, size_t sk_size) {
901
902 auto license = commercial::require_feature("Kyber decapsulate");
903 if (!license) return license.error();
904
905 if (ct_size != CIPHERTEXT_SIZE) {
907 "KyberKem: ciphertext must be "
908 + std::to_string(CIPHERTEXT_SIZE) + " bytes, got "
909 + std::to_string(ct_size)};
910 }
911 if (sk_size != SECRET_KEY_SIZE) {
913 "KyberKem: secret key must be "
914 + std::to_string(SECRET_KEY_SIZE) + " bytes, got "
915 + std::to_string(sk_size)};
916 }
917
918#ifdef SIGNET_HAS_LIBOQS
919 // --- liboqs mode: real Kyber-768 decapsulation ---
920 OQS_KEM* kem = OQS_KEM_new(kOqsKemAlgMlKem768);
921 if (!kem) {
923 "KyberKem: failed to initialize OQS Kyber-768"};
924 }
925
926 std::vector<uint8_t> shared_secret(kem->length_shared_secret);
927
928 OQS_STATUS rc = OQS_KEM_decaps(kem,
929 shared_secret.data(),
930 ciphertext,
931 secret_key);
932 OQS_KEM_free(kem);
933
934 if (rc != OQS_SUCCESS) {
936 "KyberKem: OQS decapsulation failed"};
937 }
938
939 return shared_secret;
940#else
941 // --- REFERENCE IMPLEMENTATION — use liboqs for production ---
942 //
943 // 1. Extract pk_hash from secret_key (last 32 bytes)
944 // 2. Recover seed = ciphertext[0..31] XOR pk_hash
945 // 3. shared_secret = SHA-256(seed || pk_hash)
946
947 // CR-2: Unless SIGNET_ALLOW_STUB_PQ is explicitly defined, refuse to
948 // perform actual decryption with the insecure stub implementation.
949#ifndef SIGNET_ALLOW_STUB_PQ
951 "KyberKem: stub PQ implementation provides zero security. "
952 "Build with -DSIGNET_ENABLE_PQ=ON (liboqs) for real Kyber-768, "
953 "or define SIGNET_ALLOW_STUB_PQ to allow the insecure stub."};
954#endif
955
956 // Extract pk_hash from the tail of secret_key
957 std::array<uint8_t, 32> pk_hash;
958 std::memcpy(pk_hash.data(), secret_key + SECRET_KEY_SIZE - 32, 32);
959
960 // Recover seed by XORing the first 32 bytes of ciphertext with pk_hash
961 std::array<uint8_t, 32> seed;
962 for (size_t i = 0; i < 32; ++i) {
963 seed[i] = ciphertext[i] ^ pk_hash[i];
964 }
965
966 // Derive shared secret: SHA-256(seed || pk_hash)
968 seed.data(), seed.size(), pk_hash.data(), pk_hash.size());
969
970 std::vector<uint8_t> shared_secret(SHARED_SECRET_SIZE);
971 std::memcpy(shared_secret.data(), ss.data(), SHARED_SECRET_SIZE);
972
973 return shared_secret;
974#endif
975 }
976};
977
978// ===========================================================================
979// DilithiumSign -- Dilithium-3 Digital Signatures
980//
981// Used to sign Parquet file footers for tamper detection.
982//
983// Dilithium-3 is the NIST ML-DSA-65 standard (FIPS 204), providing
984// approximately 192-bit post-quantum security for digital signatures.
985//
986// Parameter set (Dilithium-3 / ML-DSA-65):
987// Public key: liboqs-defined (1952 in current profiles)
988// Secret key: liboqs-defined (4032 for ML-DSA-65, 4000 in older Dilithium-3 builds)
989// Signature (max): liboqs-defined (3309 for ML-DSA-65, 3293 in older Dilithium-3 builds)
990// ===========================================================================
991
1005public:
1006#if defined(SIGNET_HAS_LIBOQS) && defined(OQS_SIG_ml_dsa_65_length_public_key)
1007 static constexpr size_t PUBLIC_KEY_SIZE = OQS_SIG_ml_dsa_65_length_public_key;
1008 static constexpr size_t SECRET_KEY_SIZE = OQS_SIG_ml_dsa_65_length_secret_key;
1009 static constexpr size_t SIGNATURE_MAX_SIZE = OQS_SIG_ml_dsa_65_length_signature;
1010#elif defined(SIGNET_HAS_LIBOQS) && defined(OQS_SIG_dilithium_3_length_public_key)
1011 static constexpr size_t PUBLIC_KEY_SIZE = OQS_SIG_dilithium_3_length_public_key;
1012 static constexpr size_t SECRET_KEY_SIZE = OQS_SIG_dilithium_3_length_secret_key;
1013 static constexpr size_t SIGNATURE_MAX_SIZE = OQS_SIG_dilithium_3_length_signature;
1014#else
1015 static constexpr size_t PUBLIC_KEY_SIZE = 1952;
1016 static constexpr size_t SECRET_KEY_SIZE = 4000;
1017 static constexpr size_t SIGNATURE_MAX_SIZE = 3293;
1018#endif
1019
1022 std::vector<uint8_t> public_key;
1023 std::vector<uint8_t> secret_key;
1024
1027 if (!secret_key.empty()) {
1028 volatile uint8_t* p = secret_key.data();
1029 for (size_t i = 0; i < secret_key.size(); ++i) p[i] = 0;
1030 }
1031 }
1032 };
1033
1034 // -----------------------------------------------------------------------
1035 // generate_keypair -- Generate a Dilithium-3 signing keypair
1036 //
1037 // Bundled mode:
1038 // Generates random bytes for the keys and embeds a hash binding
1039 // between public and secret keys for the signature stub to use.
1040 //
1041 // liboqs mode:
1042 // Delegates to OQS_SIG_dilithium_3_keypair().
1043 // -----------------------------------------------------------------------
1047 auto license = commercial::require_feature("Dilithium generate_keypair");
1048 if (!license) return license.error();
1049
1050#ifdef SIGNET_HAS_LIBOQS
1051 // --- liboqs mode: real Dilithium-3 key generation ---
1052 OQS_SIG* sig = OQS_SIG_new(kOqsSigAlgMlDsa65);
1053 if (!sig) {
1055 "DilithiumSign: failed to initialize OQS Dilithium-3"};
1056 }
1057
1058 SignKeyPair kp;
1059 kp.public_key.resize(sig->length_public_key);
1060 kp.secret_key.resize(sig->length_secret_key);
1061
1062 OQS_STATUS rc = OQS_SIG_keypair(sig,
1063 kp.public_key.data(),
1064 kp.secret_key.data());
1065 OQS_SIG_free(sig);
1066
1067 if (rc != OQS_SUCCESS) {
1069 "DilithiumSign: OQS keypair generation failed"};
1070 }
1071
1072 return kp;
1073#else
1074#ifndef SIGNET_ALLOW_STUB_PQ
1076 "DilithiumSign: stub PQ implementation provides zero security. "
1077 "Install liboqs and rebuild with -DSIGNET_ENABLE_PQ=ON, "
1078 "or define SIGNET_ALLOW_STUB_PQ to allow the insecure stub."};
1079#else
1080 // --- TEST-ONLY STUB IMPLEMENTATION — use liboqs for production ---
1081 //
1082 // Generates random keys with an embedded binding:
1083 // secret_key layout: [random(3936)] [SHA-256(pk_seed)(32)] [pk_seed(32)]
1084 // public_key layout: [pk_seed(32)] [random(1920)]
1085 //
1086 // The pk_seed is shared between both keys so that sign() can produce
1087 // a deterministic "signature" that verify() can check.
1088
1089 SignKeyPair kp;
1092
1093 // Embed the first 32 bytes of public key (pk_seed) at the end of
1094 // secret key, and its hash before it, for the stub signature scheme.
1095 auto pk_seed_hash = detail::sha256::sha256(
1096 kp.public_key.data(), 32);
1097
1098 // sk[3968..3999] = pk_seed (first 32 bytes of pk)
1099 std::memcpy(kp.secret_key.data() + SECRET_KEY_SIZE - 32,
1100 kp.public_key.data(), 32);
1101
1102 // sk[3936..3967] = SHA-256(pk_seed)
1103 std::memcpy(kp.secret_key.data() + SECRET_KEY_SIZE - 64,
1104 pk_seed_hash.data(), 32);
1105
1106 return kp;
1107#endif
1108#endif
1109 }
1110
1111 // -----------------------------------------------------------------------
1112 // sign -- Sign a message with the secret key
1113 //
1114 // Produces a signature of up to SIGNATURE_MAX_SIZE bytes.
1115 //
1116 // Bundled mode:
1117 // Produces a "signature" = SHA-256(sk_binding || message) padded to
1118 // a fixed size. This is NOT a real Dilithium lattice signature and
1119 // provides NO post-quantum security. Any party with the secret key
1120 // binding can forge signatures.
1121 //
1122 // liboqs mode:
1123 // Delegates to OQS_SIG_dilithium_3_sign().
1124 // -----------------------------------------------------------------------
1133 const uint8_t* message, size_t msg_size,
1134 const uint8_t* secret_key, size_t sk_size) {
1135
1136 auto license = commercial::require_feature("Dilithium sign");
1137 if (!license) return license.error();
1138
1139 if (sk_size != SECRET_KEY_SIZE) {
1141 "DilithiumSign: secret key must be "
1142 + std::to_string(SECRET_KEY_SIZE) + " bytes, got "
1143 + std::to_string(sk_size)};
1144 }
1145
1146#ifdef SIGNET_HAS_LIBOQS
1147 // --- liboqs mode: real Dilithium-3 signing ---
1148 OQS_SIG* sig = OQS_SIG_new(kOqsSigAlgMlDsa65);
1149 if (!sig) {
1151 "DilithiumSign: failed to initialize OQS Dilithium-3"};
1152 }
1153
1154 std::vector<uint8_t> signature(sig->length_signature);
1155 size_t sig_len = 0;
1156
1157 OQS_STATUS rc = OQS_SIG_sign(sig,
1158 signature.data(), &sig_len,
1159 message, msg_size,
1160 secret_key);
1161 OQS_SIG_free(sig);
1162
1163 if (rc != OQS_SUCCESS) {
1165 "DilithiumSign: OQS signing failed"};
1166 }
1167
1168 signature.resize(sig_len);
1169 return signature;
1170#else
1171#ifndef SIGNET_ALLOW_STUB_PQ
1173 "DilithiumSign: stub PQ implementation provides zero security. "
1174 "Install liboqs and rebuild with -DSIGNET_ENABLE_PQ=ON, "
1175 "or define SIGNET_ALLOW_STUB_PQ to allow the insecure stub."};
1176#else
1177 // --- TEST-ONLY STUB IMPLEMENTATION — use liboqs for production ---
1178 //
1179 // "Signature" = SHA-256(pk_seed_hash || message), zero-padded to
1180 // SIGNATURE_MAX_SIZE bytes.
1181 //
1182 // pk_seed_hash is extracted from sk[3936..3967].
1183 // This is a MAC, NOT a digital signature. It proves knowledge of
1184 // the secret key but does NOT provide the non-repudiation or
1185 // unforgeability guarantees of a real Dilithium signature.
1186
1187 // Extract the pk_seed_hash from the secret key
1188 std::array<uint8_t, 32> pk_seed_hash;
1189 std::memcpy(pk_seed_hash.data(),
1190 secret_key + SECRET_KEY_SIZE - 64, 32);
1191
1192 // Compute "signature" = SHA-256(pk_seed_hash || message)
1193 auto sig_hash = detail::sha256::sha256_concat(
1194 pk_seed_hash.data(), pk_seed_hash.size(),
1195 message, msg_size);
1196
1197 // Pad to SIGNATURE_MAX_SIZE
1198 // Layout: [SHA-256 hash (32 bytes)] [deterministic padding (3261 bytes)]
1199 std::vector<uint8_t> signature(SIGNATURE_MAX_SIZE, 0);
1200 std::memcpy(signature.data(), sig_hash.data(), 32);
1201
1202 // Fill padding deterministically from the hash so verify() can
1203 // check it (the padding is derived, not random)
1204 for (size_t i = 32; i < SIGNATURE_MAX_SIZE; ++i) {
1205 signature[i] = sig_hash[i % 32] ^ static_cast<uint8_t>(i & 0xFF);
1206 }
1207
1208 return signature;
1209#endif
1210#endif
1211 }
1212
1213 // -----------------------------------------------------------------------
1214 // verify -- Verify a signature against a message and public key
1215 //
1216 // Returns true if the signature is valid, false otherwise.
1217 //
1218 // Bundled mode:
1219 // Recomputes the expected "signature" from the public key's pk_seed
1220 // and message, then compares in constant time.
1221 //
1222 // liboqs mode:
1223 // Delegates to OQS_SIG_dilithium_3_verify().
1224 // -----------------------------------------------------------------------
1234 [[nodiscard]] static expected<bool> verify(
1235 const uint8_t* message, size_t msg_size,
1236 const uint8_t* signature, size_t sig_size,
1237 const uint8_t* public_key, size_t pk_size) {
1238
1239 auto license = commercial::require_feature("Dilithium verify");
1240 if (!license) return license.error();
1241
1242 if (pk_size != PUBLIC_KEY_SIZE) {
1244 "DilithiumSign: public key must be "
1245 + std::to_string(PUBLIC_KEY_SIZE) + " bytes, got "
1246 + std::to_string(pk_size)};
1247 }
1248 if (sig_size == 0 || sig_size > SIGNATURE_MAX_SIZE) {
1250 "DilithiumSign: invalid signature size "
1251 + std::to_string(sig_size)};
1252 }
1253
1254#ifdef SIGNET_HAS_LIBOQS
1255 // --- liboqs mode: real Dilithium-3 verification ---
1256 OQS_SIG* sig_ctx = OQS_SIG_new(kOqsSigAlgMlDsa65);
1257 if (!sig_ctx) {
1259 "DilithiumSign: failed to initialize OQS Dilithium-3"};
1260 }
1261
1262 OQS_STATUS rc = OQS_SIG_verify(sig_ctx,
1263 message, msg_size,
1264 signature, sig_size,
1265 public_key);
1266 OQS_SIG_free(sig_ctx);
1267
1268 return (rc == OQS_SUCCESS);
1269#else
1270#ifndef SIGNET_ALLOW_STUB_PQ
1272 "DilithiumSign: stub PQ implementation provides zero security. "
1273 "Install liboqs and rebuild with -DSIGNET_ENABLE_PQ=ON, "
1274 "or define SIGNET_ALLOW_STUB_PQ to allow the insecure stub."};
1275#else
1276 // --- TEST-ONLY STUB IMPLEMENTATION — use liboqs for production ---
1277 //
1278 // Recompute the expected "signature" from pk_seed and message,
1279 // then compare the first 32 bytes (the SHA-256 core) in constant time.
1280
1281 // Extract pk_seed from the first 32 bytes of the public key
1282 auto pk_seed_hash = detail::sha256::sha256(public_key, 32);
1283
1284 // Recompute expected signature hash
1285 auto expected_hash = detail::sha256::sha256_concat(
1286 pk_seed_hash.data(), pk_seed_hash.size(),
1287 message, msg_size);
1288
1289 // Constant-time comparison of the first 32 bytes
1290 // (the SHA-256 hash is the cryptographic core of our stub signature)
1291 if (sig_size < 32) {
1292 return false;
1293 }
1294
1295 uint8_t diff = 0;
1296 for (size_t i = 0; i < 32; ++i) {
1297 diff |= signature[i] ^ expected_hash[i];
1298 }
1299
1300 return (diff == 0);
1301#endif
1302#endif
1303 }
1304};
1305
1306// ===========================================================================
1307// HybridKem -- Kyber-768 + X25519 Hybrid Key Encapsulation
1308//
1309// Combines post-quantum (Kyber-768) and classical (X25519) key exchange
1310// into a single hybrid KEM. The shared secret is derived as:
1311//
1312// shared_secret = SHA-256(kyber_shared_secret || x25519_shared_secret)
1313//
1314// This provides defense-in-depth: even if Kyber is broken (unlikely given
1315// NIST standardization), X25519 still provides classical security. And
1316// if X25519 is broken by a quantum computer, Kyber still provides
1317// post-quantum security.
1318//
1319// This follows the "KEM combiner" approach recommended by NIST and the
1320// IETF Composite KEM draft.
1321//
1322// X25519 implementation:
1323// Uses the constant-time Montgomery ladder in detail::x25519 (RFC 7748).
1324// DH commutativity: X25519(eph_sk, recip_pk) == X25519(recip_sk, eph_pk).
1325// This is correct in both bundled mode and liboqs mode.
1326// ===========================================================================
1327
1340public:
1341 static constexpr size_t X25519_PUBLIC_KEY_SIZE = 32;
1342 static constexpr size_t X25519_SECRET_KEY_SIZE = 32;
1343 static constexpr size_t HYBRID_SHARED_SECRET_SIZE = 32;
1344
1347 std::vector<uint8_t> kyber_public_key;
1348 std::vector<uint8_t> kyber_secret_key;
1349 std::vector<uint8_t> x25519_public_key;
1350 std::vector<uint8_t> x25519_secret_key;
1351
1354 if (!kyber_secret_key.empty()) {
1355 volatile uint8_t* p = kyber_secret_key.data();
1356 for (size_t i = 0; i < kyber_secret_key.size(); ++i) p[i] = 0;
1357 }
1358 if (!x25519_secret_key.empty()) {
1359 volatile uint8_t* p = x25519_secret_key.data();
1360 for (size_t i = 0; i < x25519_secret_key.size(); ++i) p[i] = 0;
1361 }
1362 }
1363 };
1364
1367 std::vector<uint8_t> kyber_ciphertext;
1368 std::vector<uint8_t> x25519_public_key;
1369 std::vector<uint8_t> shared_secret;
1370 };
1371
1372 // -----------------------------------------------------------------------
1373 // generate_keypair
1374 // Generates Kyber-768 + real X25519 (RFC 7748) keypair.
1375 // -----------------------------------------------------------------------
1379 auto license = commercial::require_feature("HybridKem generate_keypair");
1380 if (!license) return license.error();
1381
1382 // Kyber-768 component (stub or real via liboqs)
1383 auto kyber_result = KyberKem::generate_keypair();
1384 if (!kyber_result) return kyber_result.error();
1385
1386 HybridKeyPair hkp;
1387 hkp.kyber_public_key = std::move(kyber_result->public_key);
1388 hkp.kyber_secret_key = std::move(kyber_result->secret_key);
1389
1390 // X25519 component: real RFC 7748 Curve25519 keypair
1391 auto x25519_kp = detail::x25519::generate_keypair();
1392 if (!x25519_kp) return x25519_kp.error();
1393
1394 hkp.x25519_secret_key.assign(x25519_kp->first.begin(), x25519_kp->first.end());
1395 hkp.x25519_public_key.assign(x25519_kp->second.begin(), x25519_kp->second.end());
1396
1397 return hkp;
1398 }
1399
1400 // -----------------------------------------------------------------------
1401 // encapsulate
1402 //
1403 // Kyber-768 encapsulation (stub or liboqs) + real X25519 DH:
1404 // 1. kyber_ss from KyberKem::encapsulate(recipient.kyber_pk)
1405 // 2. Generate ephemeral X25519 keypair (eph_sk, eph_pk)
1406 // 3. x25519_ss = X25519(eph_sk, recipient.x25519_pk) -- real DH
1407 // 4. shared_secret = SHA-256(kyber_ss || x25519_ss)
1408 // 5. Send: kyber_ciphertext + eph_pk
1409 //
1410 // The recipient runs decapsulate() with their full keypair to
1411 // recover the same shared_secret because X25519 is commutative:
1412 // X25519(eph_sk, recip_pk) == X25519(recip_sk, eph_pk)
1413 // -----------------------------------------------------------------------
1419 const HybridKeyPair& recipient_pk) {
1420
1421 auto license = commercial::require_feature("HybridKem encapsulate");
1422 if (!license) return license.error();
1423
1424 // Step 1: Kyber-768 encapsulation
1425 auto kyber_result = KyberKem::encapsulate(
1426 recipient_pk.kyber_public_key.data(),
1427 recipient_pk.kyber_public_key.size());
1428 if (!kyber_result) return kyber_result.error();
1429
1430 // Step 2: Generate ephemeral X25519 keypair
1431 auto eph_kp = detail::x25519::generate_keypair();
1432 if (!eph_kp) return eph_kp.error();
1433 const auto& eph_sk = eph_kp->first;
1434 const auto& eph_pk = eph_kp->second;
1435
1436 // Step 3: X25519 shared secret = X25519(eph_sk, recipient.x25519_pk)
1437 if (recipient_pk.x25519_public_key.size() != 32) {
1439 "HybridKem: recipient X25519 public key must be 32 bytes"};
1440 }
1441 std::array<uint8_t, 32> recip_x25519_pk;
1442 std::memcpy(recip_x25519_pk.data(),
1443 recipient_pk.x25519_public_key.data(), 32);
1444
1445 auto x25519_ss_result = detail::x25519::x25519(eph_sk, recip_x25519_pk);
1446 if (!x25519_ss_result) return x25519_ss_result.error();
1447
1448 // Step 4: Combined shared secret = SHA-256(label || kyber_ss || x25519_ss)
1449 // Domain separation per NIST SP 800-227 (Final, Sep 2025) §4.2 hybrid combiner
1450 auto combined = detail::sha256::sha256_concat(
1451 kyber_result->shared_secret.data(),
1452 kyber_result->shared_secret.size(),
1453 x25519_ss_result->data(),
1454 x25519_ss_result->size());
1455
1456 HybridEncapsResult result;
1457 result.kyber_ciphertext = std::move(kyber_result->ciphertext);
1458 result.x25519_public_key.assign(eph_pk.begin(), eph_pk.end());
1459 result.shared_secret.assign(combined.begin(), combined.end());
1460
1461 return result;
1462 }
1463
1464 // -----------------------------------------------------------------------
1465 // decapsulate
1466 //
1467 // Recovers the same shared_secret as encapsulate() because X25519 DH
1468 // is commutative: X25519(eph_sk, recip_pk) == X25519(recip_sk, eph_pk).
1469 //
1470 // 1. kyber_ss = KyberKem::decapsulate(kyber_ciphertext, recip.kyber_sk)
1471 // 2. x25519_ss = X25519(recip.x25519_sk, eph_pk) -- commutative with encaps
1472 // 3. shared_secret = SHA-256(kyber_ss || x25519_ss) -- identical to encaps
1473 //
1474 // Works in both bundled mode (Kyber stub + real X25519) and
1475 // liboqs mode (real Kyber + real X25519).
1476 // -----------------------------------------------------------------------
1484 const HybridEncapsResult& encaps,
1485 const HybridKeyPair& recipient_sk) {
1486
1487 auto license = commercial::require_feature("HybridKem decapsulate");
1488 if (!license) return license.error();
1489
1490 // Step 1: Kyber-768 decapsulation
1491 auto kyber_ss = KyberKem::decapsulate(
1492 encaps.kyber_ciphertext.data(),
1493 encaps.kyber_ciphertext.size(),
1494 recipient_sk.kyber_secret_key.data(),
1495 recipient_sk.kyber_secret_key.size());
1496 if (!kyber_ss) return kyber_ss.error();
1497
1498 // Step 2: X25519 key agreement (commutative with encapsulate)
1499 if (recipient_sk.x25519_secret_key.size() != 32 ||
1500 encaps.x25519_public_key.size() != 32) {
1502 "HybridKem: X25519 key sizes must be 32 bytes"};
1503 }
1504 std::array<uint8_t, 32> recip_sk_arr, eph_pk_arr;
1505 std::memcpy(recip_sk_arr.data(), recipient_sk.x25519_secret_key.data(), 32);
1506 std::memcpy(eph_pk_arr.data(), encaps.x25519_public_key.data(), 32);
1507
1508 auto x25519_ss_result = detail::x25519::x25519(recip_sk_arr, eph_pk_arr);
1509 if (!x25519_ss_result) return x25519_ss_result.error();
1510
1511 // Step 3: Combined shared secret — identical to encapsulate()
1512 // Domain separation per NIST SP 800-227 (Final, Sep 2025) §4.2 hybrid combiner
1513 auto combined = detail::sha256::sha256_concat(
1514 kyber_ss->data(), kyber_ss->size(),
1515 x25519_ss_result->data(), x25519_ss_result->size());
1516
1517 return std::vector<uint8_t>(combined.begin(), combined.end());
1518 }
1519};
1520
1521// ===========================================================================
1522// PostQuantumConfig -- Configuration for post-quantum encryption in PME
1523//
1524// This structure integrates with the existing EncryptionConfig to add
1525// post-quantum key encapsulation and footer signing.
1526//
1527// Usage:
1528// PostQuantumConfig pq_cfg;
1529// pq_cfg.enabled = true;
1530// pq_cfg.hybrid_mode = true; // Kyber + X25519
1531//
1532// // Generate keypairs (typically done once, stored securely)
1533// auto kem_kp = KyberKem::generate_keypair();
1534// pq_cfg.recipient_public_key = kem_kp->public_key;
1535// pq_cfg.recipient_secret_key = kem_kp->secret_key;
1536//
1537// auto sig_kp = DilithiumSign::generate_keypair();
1538// pq_cfg.signing_public_key = sig_kp->public_key;
1539// pq_cfg.signing_secret_key = sig_kp->secret_key;
1540//
1541// When writing:
1542// 1. encapsulate() with recipient_public_key to get the AES-256 key
1543// 2. Use that key as the footer_key / column_key in EncryptionConfig
1544// 3. sign() the serialized footer with signing_secret_key
1545// 4. Store the KEM ciphertext and signature in file metadata
1546//
1547// When reading:
1548// 1. decapsulate() with recipient_secret_key to recover the AES-256 key
1549// 2. Use that key to decrypt footer / columns via PME
1550// 3. verify() the footer signature with signing_public_key
1551// ===========================================================================
1552
1562 bool enabled = false;
1563
1568 bool hybrid_mode = true;
1569
1570 // --- Kyber KEM keypair for file encryption key exchange ---
1571
1574 std::vector<uint8_t> recipient_public_key;
1575
1579 std::vector<uint8_t> recipient_secret_key;
1580
1581 // --- Dilithium signing keypair for footer signing ---
1582
1585 std::vector<uint8_t> signing_public_key;
1586
1590 std::vector<uint8_t> signing_secret_key;
1591
1594 auto zero_vec = [](std::vector<uint8_t>& v) {
1595 if (!v.empty()) {
1596 volatile uint8_t* p = v.data();
1597 for (size_t i = 0; i < v.size(); ++i) p[i] = 0;
1598 }
1599 };
1600 zero_vec(recipient_secret_key);
1601 zero_vec(signing_secret_key);
1602 }
1603};
1604
1605} // namespace signet::forge::crypto
Abstract cipher interface, GCM/CTR adapters, CipherFactory, and platform CSPRNG.
Dilithium-3 digital signature scheme (NIST FIPS 204 / ML-DSA-65).
static expected< std::vector< uint8_t > > sign(const uint8_t *message, size_t msg_size, const uint8_t *secret_key, size_t sk_size)
Sign a message with the secret key.
static expected< bool > verify(const uint8_t *message, size_t msg_size, const uint8_t *signature, size_t sig_size, const uint8_t *public_key, size_t pk_size)
Verify a signature against a message and public key.
static constexpr size_t SIGNATURE_MAX_SIZE
Maximum Dilithium-3 signature size (stub default).
static constexpr size_t SECRET_KEY_SIZE
Dilithium-3 secret key size (stub default).
static constexpr size_t PUBLIC_KEY_SIZE
Dilithium-3 public key size (stub default).
static expected< SignKeyPair > generate_keypair()
Generate a Dilithium-3 signing keypair.
Hybrid Key Encapsulation combining Kyber-768 (post-quantum) and X25519 (classical).
static expected< HybridKeyPair > generate_keypair()
Generate a hybrid Kyber-768 + X25519 (RFC 7748) keypair.
static expected< HybridEncapsResult > encapsulate(const HybridKeyPair &recipient_pk)
Hybrid encapsulation: Kyber-768 + X25519 DH key agreement.
static constexpr size_t HYBRID_SHARED_SECRET_SIZE
Combined shared secret size (SHA-256 output).
static constexpr size_t X25519_PUBLIC_KEY_SIZE
X25519 public key size in bytes.
static constexpr size_t X25519_SECRET_KEY_SIZE
X25519 secret key size in bytes.
static expected< std::vector< uint8_t > > decapsulate(const HybridEncapsResult &encaps, const HybridKeyPair &recipient_sk)
Hybrid decapsulation: recovers the same shared secret as encapsulate().
Kyber-768 Key Encapsulation Mechanism (NIST FIPS 203 / ML-KEM-768).
static constexpr size_t SECRET_KEY_SIZE
Kyber-768 secret key size (stub default).
static expected< KeyPair > generate_keypair()
Generate a Kyber-768 keypair.
static expected< std::vector< uint8_t > > decapsulate(const uint8_t *ciphertext, size_t ct_size, const uint8_t *secret_key, size_t sk_size)
Recover the shared secret from ciphertext + secret key (decapsulation).
static constexpr size_t PUBLIC_KEY_SIZE
Kyber-768 public key size (stub default).
static constexpr size_t CIPHERTEXT_SIZE
Kyber-768 ciphertext size (stub default).
static expected< EncapsulationResult > encapsulate(const uint8_t *public_key, size_t pk_size)
Generate a shared secret from a recipient's public key (encapsulation).
static constexpr size_t SHARED_SECRET_SIZE
Shared secret size (256 bits, for AES-256).
A lightweight result type that holds either a success value of type T or an Error.
Definition error.hpp:145
void fill_random_bytes(uint8_t *buf, size_t size)
Fill a buffer with cryptographically random bytes using the best available OS-level CSPRNG (CWE-338: ...
void random_bytes(uint8_t *buf, size_t size)
Fill a buffer with cryptographically random bytes.
std::array< uint8_t, 32 > sha256_concat(const uint8_t *a, size_t a_size, const uint8_t *b, size_t b_size)
Hash concatenation of two byte spans with domain separation: SHA-256(label || a || b).
Definition sha256.hpp:226
std::array< uint8_t, 32 > sha256(const uint8_t *data, size_t size)
Compute SHA-256 hash of arbitrary-length input.
Definition sha256.hpp:165
std::array< uint8_t, 32 > x25519_raw(const std::array< uint8_t, 32 > &scalar, const std::array< uint8_t, 32 > &point_u)
X25519 scalar multiplication: result = scalar * point.
std::array< int32_t, 10 > Fe
GF(2^255-19) field element: 10 limbs, radix 2^25.5 (alternating 26 and 25 bits).
Fe fe_from_bytes(const uint8_t *b)
Load 32 little-endian bytes into a 10-limb field element.
expected< std::array< uint8_t, 32 > > x25519(const std::array< uint8_t, 32 > &scalar, const std::array< uint8_t, 32 > &u_coord)
Compute X25519(scalar, u_coord).
void fe_cswap(Fe &a, Fe &b, uint64_t swap)
void fe_to_bytes(uint8_t *out, Fe h)
std::array< uint8_t, 32 > clamp_scalar(std::array< uint8_t, 32 > k)
Clamp a 32-byte scalar per RFC 7748 §5.
const std::array< uint8_t, 32 > & base_point()
The X25519 base point u=9, encoded as 32 LE bytes.
Fe fe_mul(const Fe &f, const Fe &g)
expected< std::pair< std::array< uint8_t, 32 >, std::array< uint8_t, 32 > > > generate_keypair()
Generate a new X25519 keypair.
bool is_real_pq_crypto() noexcept
Runtime query: returns true if post-quantum crypto is backed by real liboqs implementations (Kyber-76...
@ ENCRYPTION_ERROR
An encryption or decryption operation failed (bad key, tampered ciphertext, PME error).
SHA-256 hash function (NIST FIPS 180-4).
Lightweight error value carrying an ErrorCode and a human-readable message.
Definition error.hpp:101
Dilithium-3 signing keypair: public key for verification, secret key for signing.
std::vector< uint8_t > public_key
PUBLIC_KEY_SIZE bytes.
std::vector< uint8_t > secret_key
SECRET_KEY_SIZE bytes.
~SignKeyPair()
Zeroing destructor (CWE-244: heap inspection).
std::vector< uint8_t > x25519_public_key
Ephemeral X25519 public key (32 bytes, sent to recipient).
std::vector< uint8_t > kyber_ciphertext
Kyber ciphertext (1088 bytes, sent to recipient).
std::vector< uint8_t > shared_secret
32 bytes = SHA-256(kyber_ss || x25519_ss).
Hybrid keypair: Kyber-768 + X25519 components.
std::vector< uint8_t > x25519_secret_key
X25519 clamped secret scalar (32 bytes).
~HybridKeyPair()
Zeroing destructor (CWE-244: heap inspection).
std::vector< uint8_t > kyber_secret_key
Kyber-768 secret key (2400 bytes).
std::vector< uint8_t > kyber_public_key
Kyber-768 public key (1184 bytes).
std::vector< uint8_t > x25519_public_key
X25519 public key (32 bytes).
Result of Kyber-768 encapsulation: ciphertext to send + shared secret to keep.
std::vector< uint8_t > ciphertext
CIPHERTEXT_SIZE bytes (sent to recipient).
std::vector< uint8_t > shared_secret
32 bytes (used as AES-256 key).
Kyber-768 keypair: public key for encapsulation, secret key for decapsulation.
~KeyPair()
Zeroing destructor (CWE-244: heap inspection, NIST SP 800-38D §8.3).
std::vector< uint8_t > public_key
PUBLIC_KEY_SIZE bytes.
std::vector< uint8_t > secret_key
SECRET_KEY_SIZE bytes.
Configuration for post-quantum encryption in Parquet Modular Encryption.
std::vector< uint8_t > recipient_secret_key
Recipient's Kyber-768 secret key (KyberKem::SECRET_KEY_SIZE bytes).
std::vector< uint8_t > signing_secret_key
Dilithium-3/ML-DSA-65 secret key (DilithiumSign::SECRET_KEY_SIZE bytes).
bool enabled
Master enable for post-quantum features.
std::vector< uint8_t > signing_public_key
Dilithium-3/ML-DSA-65 public key (DilithiumSign::PUBLIC_KEY_SIZE bytes).
~PostQuantumConfig()
Zeroing destructor (CWE-244: heap inspection).
bool hybrid_mode
When true, use hybrid KEM (Kyber-768 + X25519) for key exchange.
std::vector< uint8_t > recipient_public_key
Recipient's Kyber-768 public key (KyberKem::PUBLIC_KEY_SIZE bytes).