Primitives¶
Cryptographic building blocks of the schemes: lattices
(lattice), Gaussian sampling
(sampling), hashing
(hashing), groups
(groups) and pairings
(pairing).
Lattices¶
LWE and Ring-LWE: parameters and security tables, NTTs, seeded public matrices and arithmetic modulo q.
LWE Parameters¶
LWE parameter derivation, vector centering, and inner-product decoding.
- pyfe4ai.primitives.lattice.lwe_utils.derive_lwe_parameters(max_plain_bound, bound_y, eta, lwe_n, extra_dimension=None)[source]¶
Derive LWE scheme parameters from plaintext and weight bounds.
- pyfe4ai.primitives.lattice.lwe_utils.derive_fullysec_lwe_parameters(bound_x, bound_y, eta, lwe_n)[source]¶
Derive fully-secure LWE parameters with multiple noise levels.
- pyfe4ai.primitives.lattice.lwe_utils.center_lwe_vector(vector, p, q)[source]¶
Encode a signed vector as
floor(q / p) x mod q.The encoding is linear in
x, as the linear ciphertext homomorphism of the ABDP15 PKE requires (versions <= 0.2.0 usedtrunc(x q / p), which differs from it by a small non-linear term).
- pyfe4ai.primitives.lattice.lwe_utils.decode_lwe_inner_product(value, p, q)[source]¶
Extract a plaintext value from an LWE inner-product ciphertext.
- pyfe4ai.primitives.lattice.lwe_utils.DENSE_LWE_128_LOG_Q¶
Largest
q.bit_length()for which LWE in dimensionnwith a uniform secret moduloq, errorsD_{Z, ceil(2 sqrt(n))}and any number of samples costs at least 2^128 operations, by the lattice estimator (https://github.com/malb/lattice-estimator, commit 53da598, default MATZOV cost model; primal uSVP and BDD, dual, dual hybrid). The error width is thesigma_eofabmpsw20_lwe_parameters(), so this bounds the LWE problem of the ABMPSW20 security proof. Regenerate withtools/params/estimate_dense_lwe.py.
- pyfe4ai.primitives.lattice.lwe_utils.check_dense_lwe(lwe_n, q)[source]¶
Raise unless LWE in dimension lwe_n modulo q meets
DENSE_LWE_128_LOG_Q.A dimension between two tabulated ones is held to the bound of the smaller one (LWE only gets harder with the dimension and with the error width
2 sqrt(n)).
- pyfe4ai.primitives.lattice.lwe_utils.dense_lwe_n(modulus_for)[source]¶
Smallest tabulated dimension whose modulus
modulus_for(n)meetsDENSE_LWE_128_LOG_Q(the modulus grows with the dimension).- Return type:
- pyfe4ai.primitives.lattice.lwe_utils.resolve_lwe_n(lwe_n, modulus_for, default)[source]¶
LWE dimension of a config entry:
"auto"selects the smallest one meetingDENSE_LWE_128_LOG_Q(seedense_lwe_n()),Nonegives default.
- pyfe4ai.primitives.lattice.lwe_utils.WIDE_LWE_128_LOG_RATIO¶
For each error width
2^s(outer key) and LWE dimensionn(inner key): the largestlog2(q / sigma)for which LWE with a uniform secret, errorsD_{Z, 2^s}and any number of samples costs at least 2^128 operations, by the lattice estimator (commit 53da598, MATZOV cost model; primal uSVP and BDD, dual). The ABDP15 public keyA S + Ehas very wide errors (sigmaof 2^30 to 2^45 for small bounds from the GoFE parameter recipe, more for large ones); its key generators read the table at the narrower widthsigma'of the LWE problem their security proof reduces to (abdp15_reduction_sigma()). At a fixed ratio the attacks get more expensive as the width grows, so a scheme is held to the row of the largest width not above the one it is checked at. Regenerate withtools/params/estimate_wide_lwe.py.
- pyfe4ai.primitives.lattice.lwe_utils.wide_lwe_bound(lwe_n, sigma)[source]¶
Largest 128-bit
log2(q / sigma)ofWIDE_LWE_128_LOG_RATIOfor lwe_n and sigma (rows at or below both), orNonebelow the table.
- pyfe4ai.primitives.lattice.lwe_utils.abdp15_reduction_sigma(sigma, diff_bound, eta)[source]¶
Error width
sigma'of the LWE problem that the ABDP15 proof reduces to.The selective-security proof of the LWE instantiation is the ABDP15 full version (ePrint 2015/017, Oct. 2015), Thm. 4.4 with Thms. 6.6 and 6.7 (Construction 6.5, Sec. 6.2), written out in F. Bourse, PhD thesis, ENS 2017, Thm. 4.3.5 with Thms. 8.2.2, 8.2.3, 8.3.2. It rewrites the public keys as
d_i pk + pk'_ifor the differenced = x1 - x0of the challenge vectors.It needs
sigma >= (1 + B sqrt(eta)) sigma'with|d_i| <= B: the condition derived in the proof of Thm. 6.6 (= Bourse, Thm. 8.2.2). The parameter list of the full version’s Sec. 6.2 states the stronger, sufficientsigma >= (l B + 1) sigma'. The scheme is then as hard as LWE with errors of widthsigma', notsigma.Notes on how the library meets the paper’s hypotheses:
The literal
p > l B^2of Thm. 4.4 / Sec. 6.2 is not needed for the Regev instance. The change of basis acts on secret keys mod the LWE primeq, and decoding the integer inner product (p > 2 eta X Y + 1) makes admissible keys orthogonal todoverZ.The thesis’s LWE hardness bound (an absolute width above
2 sqrt(n)), which the full version does not state, is met understandard:sigma' >= 2^16, the table’s smallest width.Theorem 2 of the proceedings version does not cover noisy LWE keys.
- pyfe4ai.primitives.lattice.lwe_utils.check_wide_lwe(lwe_n, q, sigma, what='sigma')[source]¶
Raise unless LWE(lwe_n, q, sigma) meets
WIDE_LWE_128_LOG_RATIO; what names the width in the message.
- pyfe4ai.primitives.lattice.lwe_utils.wide_lwe_n(params_for, lwe_sigma)[source]¶
params_for(n)(a dict withqandsigma_q) for the smallest tabulatednthat meetsWIDE_LWE_128_LOG_RATIOat the widthlwe_sigma(params).lwe_sigma has no default, so that a caller cannot fall back to the public key’s own width by omission. The ABDP15 key generators pass the width of their security reduction (
abdp15_reduction_sigma()).- Return type:
- pyfe4ai.primitives.lattice.lwe_utils.abdp15_parameters(bound_x, bound_y, eta, lwe_n, extra_dimension=None, lwe_sigma=None)[source]¶
derive_lwe_parameters()as a dict, for lwe_n or, if it is"auto", for the smallest dimension meetingWIDE_LWE_128_LOG_RATIOat the widthlwe_sigma(params)(seewide_lwe_n(); the key generators pass the width of their security reduction,abdp15_reduction_sigma()).- Return type:
- Returns:
Dict with
lwe_n,p,q,m,sigma_qandl_sigma.- Raises:
ValueError – For
lwe_n = "auto"without lwe_sigma.- Parameters:
- pyfe4ai.primitives.lattice.lwe_utils.GAUSSIAN_TAIL_CUT = 12¶
Gaussian tail cut used in worst-case bounds. A Gaussian sample exceeds 12 standard deviations in absolute value with probability below 2^-100.
- pyfe4ai.primitives.lattice.lwe_utils.abmpsw20_lwe_parameters(n_slots, bound_x, bound_y, lwe_n, stat_param, security='selective')[source]¶
Derive parameters of the ABMPSW20 LWE-based MCFE (SCN 2020, Fig. 4).
The paper’s message and key spaces
{0..P-1}and{0..V-1}are replaced by the signed ranges|x| <= Xand|y| <= Y; the inner product then lies in[-T, T]withT = n X Yand is encoded withK = 2T + 1and decoded centred. The constraints of Appendix C.2/C.3 are then instantiated as:correctness,
q0 > K (nV + 1):q0is the smallest power of two>= 4 K (n Y + 1)(a factor 4 of slack, half of which is reserved for the noise of threshold decryption);statistical step (Lemma 34/35),
sigma >= 10 n P^2withP = 2X + 1bounding||x^1 - x^0||:sigma_t = 10 n P^2;LWE noise of the proof,
alpha' q >= sqrt(n0):sigma_e = ceil(2 sqrt(n0));rounding absorbs
t . e(Lemma 28 / game G2),q >= q0 B n0^{omega(1)}:B = m0 (c sigma_t)(c sigma_e)with the tail cutcandn0^{omega(1)}instantiated as2^stat_param;qis the next prime;leftover hash lemma (Lemma 33),
m0 log2(4/3) >= log2 q + 2 kappa(selective) or>= log2 q + 2 kappa + 4 n log2 P(adaptive).
m0andqdepend on each other and are iterated to a fixed point. In the adaptive settingm0grows linearly inn log2 P(about 4,600 for 100 slots withX = Y = 100,kappa = 128), and every label costsn0 + m0hash outputs and inner-product terms, so large ndarray workloads (one label per element) become slow. The LWE dimensionn0is taken as given;DENSE_LWE_128_LOG_Qboundsqfor 128-bit hardness of LWE(n0, q, sigma_e).- Parameters:
n_slots (
int) – Total number of slotsn(clients x coordinates).bound_x (
int) – Plaintext boundX.bound_y (
int) – Weight boundY.lwe_n (
int) – LWE secret dimensionn0.stat_param (
int) – Statistical security parameterkappa.security (
str, default:'selective') –"selective"or"adaptive"(choice ofm0).
- Return type:
- Returns:
Dict with integer entries
q,q0,K,delta(floor(q / K)),bound_ip(T),n0,m0,sigma_t,sigma_eandB.
- pyfe4ai.primitives.lattice.lwe_utils.round_to_q0(value, q, q0)[source]¶
Rounding map
Z_q -> Z_q0:round((q0 / q) * (value mod q)) mod q0.- Parameters:
value – Integer (reduced mod
qfirst).q – Large modulus.
q0 – Small modulus.
- Return type:
- pyfe4ai.primitives.lattice.lwe_utils.decode_rounded_inner_product(mu_prime, q, q0, delta, bound_ip)[source]¶
Decode
mu' ~ (q0 / q) * delta * mu (mod q0)to the signedmu.Returns the integer
muclosest tomu' q / (q0 delta)after centringmu'moduloq0, orNoneif|mu| > bound_ip.
Seeded LWE Matrices¶
Public LWE matrices expanded from a seed, and exact products with them.
The ABDP15 LWE IPFE (sife/lwe, mife/lwe, mife/lwe_threshold)
publishes a uniform A in Z_q^{m x n} with m ~ n log2 q rows – about
10^8 entries at 128-bit parameters, too many for a JSON parameter file or a
pure-Python loop. Here A is published as a 32-byte seed and expanded with
SHAKE-128 (as in FrodoKEM), and the products the scheme needs are computed
exactly with numpy:
entries are held as
L = ceil(bits(q) / 31)limbs of 31 bits in float64 arrays; a sum of fewer thanMAX_ROWS = 2^22limbs stays below 2^53 and is exact in double precision, sor^T Afor a binaryr(encryption) is one BLAS product per limb;A S mod q(key generation) multiplies 16-bit pieces ofAandS, whose products summed over fewer than 2^21 columns also stay below 2^53.
matrix_mode trades memory for time:
"memory"Ais expanded once and kept,8 Lbytes per entry (about2.4 GB for
n = 1024,m = 96000and a 92-bitq); an encryption is one pass over it (about 0.1 s at that size)."stream"Ais expanded again, chunk by chunk, for every product:little memory, but every product pays the expansion (seconds at that size); a batch of encryptions shares one expansion.
Expansion format (EXPANSION_DOMAIN): rows are taken in units of
EXPANSION_ROWS. Unit u reads 4 L bytes per entry, row-major, from
SHAKE-128 of "pyfe4ai/lwe/A/v1|<seed>|<cols>|<q>|<u>" as little-endian
32-bit words, masks each to 31 bits (the last to the remaining bits of
q) and takes sum_j limb_j 2^(31 j), uniform below 2^bits(q).
Entries >= q are replaced, in order, by candidates read the same way from
that string extended with "|retry|<c>", c = 1, 2, ....
- pyfe4ai.primitives.lattice.lwe_matrix.MATRIX_MODES = ('memory', 'stream')¶
Accepted values of
matrix_mode.
- pyfe4ai.primitives.lattice.lwe_matrix.MAX_ROWS = 4194304¶
Sums of 31-bit limbs over fewer rows than this are exact in float64.
- pyfe4ai.primitives.lattice.lwe_matrix.DEFAULT_CHUNK_ROWS = 4096¶
Rows expanded at a time in
"stream"mode and forA S.
- pyfe4ai.primitives.lattice.lwe_matrix.limb_count(q)[source]¶
Number of 31-bit limbs of an integer below
2^bits(q).- Return type:
- pyfe4ai.primitives.lattice.lwe_matrix.check_matrix_mode(mode)[source]¶
Return mode if it is one of
MATRIX_MODES.
- pyfe4ai.primitives.lattice.lwe_matrix.physical_memory_bytes()[source]¶
Physical memory of this machine, or
Nonewhere the OS does not say.
- class pyfe4ai.primitives.lattice.lwe_matrix.LimbMatrix(entries, q)[source]¶
Bases:
objectAn explicit matrix over
Z_q(e.g. a public keyA S + E) held as limbs.
- class pyfe4ai.primitives.lattice.lwe_matrix.SeededMatrix(seed, rows, cols, q, mode='memory', chunk_rows=4096)[source]¶
Bases:
objectUniform
A in Z_q^{rows x cols}expanded from a hex seed (see the module docstring).- Parameters:
One instance per
(seed, shape, q, mode)and process, so that several encryptors in one process (e.g. simulated clients) expandAonce.- Parameters:
- Return type:
Ring-LWE¶
Ring-LWE helper functions shared across SIFE / MIFE / MCFE Ring-LWE schemes.
These were originally private helpers in sife/ring_lwe.py. They are
extracted here so that cross-package imports reference utils rather than
reaching into another scheme family’s private namespace.
Changed in version 0.2: Added NTT-based polynomial multiplication (O(n log n)) alongside the original naive O(n²) implementation.
Changed in version 0.3: Added next_ntt_prime for NTT-friendly parameter generation,
poly_mul auto-dispatch with cached root of unity.
- pyfe4ai.primitives.lattice.ring_lwe_utils.ntt_forward(a, q, root)[source]¶
In-place iterative Cooley-Tukey NTT (decimation-in-time).
The backend’s
ntt(): natively where the extension has a context for(q, len(a))with this root (the same values), else_ntt_forward_reference.- Args:
a: First polynomial (coefficient vector). q: Modulus (or second prime). root: Primitive root of unity for NTT.
- pyfe4ai.primitives.lattice.ring_lwe_utils.ntt_inverse(a, q, root)[source]¶
Inverse NTT: transform back to coefficient domain.
The backend’s
ntt()(natively asntt_forward()), else_ntt_inverse_reference.- Args:
a: First polynomial (coefficient vector). q: Modulus (or second prime). root: Primitive root of unity for NTT.
- pyfe4ai.primitives.lattice.ring_lwe_utils.poly_mul_ntt(a, b, q, root)[source]¶
Polynomial multiplication in Z_q[X]/(X^n+1) via NTT.
- pyfe4ai.primitives.lattice.ring_lwe_utils.RING_MODULI = ('prime', 'rns')¶
ring_modulusof the selectively secure BMMS21 schemes (MIFE / MCFE / dMCFE Ring-LWE):"prime"(default) is an NTT-friendly prime,"rns"a product of two (rns_ntt_modulus(); see RNS modulus for the Ring-LWE schemes: security review).
- pyfe4ai.primitives.lattice.ring_lwe_utils.RNS_MAX_FACTOR_BITS = 62¶
Largest bit length of an RNS factor (64-bit words of the native code, whose lazy butterflies need 4 q_i < 2^64).
- pyfe4ai.primitives.lattice.ring_lwe_utils.RNS_NATIVE_FACTORS = 4¶
Most factors of an RNS modulus that the native 256-bit ring holds (an NTT-domain value keeps one residue per 64-bit limb); moduli with more run on the Python path.
- pyfe4ai.primitives.lattice.ring_lwe_utils.check_ring_modulus(value)[source]¶
Return value if it is one of
RING_MODULI.- Return type:
- pyfe4ai.primitives.lattice.ring_lwe_utils.register_rns_modulus(q, factors)[source]¶
Record that q is the product of the distinct primes factors.
The NTT root (
_get_ntt_root) and the native context of an RNS modulus come from its factors, which the public parameters carry (q_factors); without them a compositeqhas no NTT here.- Return type:
- Returns:
The factors as a tuple of
mpz.- Raises:
FEValidationError – Unless the factors are at least two distinct primes whose product is q.
- pyfe4ai.primitives.lattice.ring_lwe_utils.rns_factors(q)[source]¶
The registered factors of q (
register_rns_modulus()), orNone.
- pyfe4ai.primitives.lattice.ring_lwe_utils.prev_ntt_prime(upper_bound, ring_n)[source]¶
The largest prime
q <= upper_boundwithq = 1 (mod 2 ring_n), orNone.
- pyfe4ai.primitives.lattice.ring_lwe_utils.rns_ntt_modulus(lower_bound, ring_n, p, factors=2)[source]¶
(q, (q_1, ..., q_k)):q = q_1 ... q_k >= lower_bound, a product ofk = factorsdistinct NTT primes, in increasing order.k - 1factors are the largest NTT primes<= floor(lower_bound^(1/k))(descending), the last the smallest NTT prime with a product>= lower_boundthat is not one of them, advanced whilegcd(floor(q / p), q) != 1(Deltais then a unit, as for a primeq; see the review). Fork = 2:q1the largest NTT prime<= sqrt(lower_bound),q2the smallest withq1 q2 >= lower_bound, as in earlier versions. Deterministic, so a cached record can be checked.- Raises:
FEValidationError – If a factor would exceed
RNS_MAX_FACTOR_BITSbits or the bound is too small for factors factors.- Parameters:
- Return type:
- pyfe4ai.primitives.lattice.ring_lwe_utils.rns_factor_count(lower_bound, min_factors=2)[source]¶
The number of RNS factors of
rns_ntt_modulus()for a modulus of the bit length of lower_bound: min_factors (two), or more so that each is at most 60 bits (four for the 212 to 218 bits of the flooded threshold moduli) – butRNS_NATIVE_FACTORS(four, of up toRNS_MAX_FACTOR_BITSbits) up to 248 bits, the most the native ring holds, rather than five.
- pyfe4ai.primitives.lattice.ring_lwe_utils.rns_ntt_modulus_for(lower_bound, ring_n, p, min_factors=2)[source]¶
rns_ntt_modulus()withrns_factor_count()factors (at least min_factors), or one more where the last factor would exceedRNS_MAX_FACTOR_BITSbits (bounds just below2^248with four factors). Deterministic.- Raises:
FEValidationError – As
rns_ntt_modulus()for both counts.- Parameters:
- Return type:
- pyfe4ai.primitives.lattice.ring_lwe_utils.poly_mul(a, b, q)[source]¶
Polynomial multiplication with automatic NTT acceleration.
The backend’s
poly_mul(): natively where the extension has a context for(q, len(a))(the same values), else_poly_mul_reference.- Args:
a: First polynomial (coefficient vector). b: Second polynomial (coefficient vector). q: Modulus (or second prime).
- class pyfe4ai.primitives.lattice.ring_lwe_utils.RingNTT(q, ring_n, root)[source]¶
Bases:
objectNTT-domain arithmetic of
Z_q[x] / (x^n + 1)withpoly_mul()’s root.inverse(mul(forward(a), forward(b)))equalspoly_mul(a, b, q), and the NTT is linear mod q, so a fixed operand can be transformed once and products summed in the NTT domain before a single inverse transform, with the same result as summing thepoly_mul()products mod q.get()returnsNonewhenqhas no primitive2n-th root of unity (thenpoly_mul()falls back to the naive product).- Parameters:
ring_n (
int)
- pyfe4ai.primitives.lattice.ring_lwe_utils.next_ntt_prime(lower_bound, ring_n)[source]¶
Find the smallest prime
q >= lower_boundwithq ≡ 1 (mod 2·ring_n).
- pyfe4ai.primitives.lattice.ring_lwe_utils.poly_add(a, b, q)[source]¶
Add two polynomials element-wise modulo q.
- pyfe4ai.primitives.lattice.ring_lwe_utils.poly_mul_negacyclic(a, b, q)[source]¶
Multiply two polynomials in the negacyclic ring Z_q[X]/(X^n+1).
- pyfe4ai.primitives.lattice.ring_lwe_utils.matrix_check_bound(matrix, bound)[source]¶
Check whether all matrix entries have absolute value within the bound.
- pyfe4ai.primitives.lattice.ring_lwe_utils.mat_vec_mul(matrix, vec, q)[source]¶
Multiply a matrix by a polynomial vector modulo q.
- pyfe4ai.primitives.lattice.ring_lwe_utils.center_matrix(matrix, p, q, ring_n)[source]¶
Encode the rows of a signed matrix as polynomials
floor(q / p) x mod q.Row
ibecomes a polynomial whose first coefficients hold the entries of the row. The encoding is linear inx, as the BMMS21 proofs need (versions <= 0.2.0 usedtrunc(x q / p)).
- pyfe4ai.primitives.lattice.ring_lwe_utils.seeded_polynomial(seed, q, ring_n, index=0)[source]¶
Uniform element of
R_qexpanded from a hex seed with SHAKE-256.The Ring-LWE schemes publish their public polynomial
athis way: a parameter cache stores only the seed, so a planted cache cannot choosea(e.g.a = 0), only another pseudo-random one.
- pyfe4ai.primitives.lattice.ring_lwe_utils.decode_vector(poly, p, q, k)[source]¶
Extract the first k plaintext coefficients by rounding.
- pyfe4ai.primitives.lattice.ring_lwe_utils.DEFAULT_RING_N = 32¶
Default ring dimension. A toy value chosen for speed; nothing here estimates the hardness of Ring-LWE at the derived (n, q, sigma), and BMMS21 (Table 1) uses n >= 2048 for comparable settings.
- pyfe4ai.primitives.lattice.ring_lwe_utils.is_values(value)[source]¶
Whether value is a sequence of values as a key holds them (a row of
ring_nvalues, a polynomial): a list, a tuple or a 1-D numpy array.- Return type:
- pyfe4ai.primitives.lattice.ring_lwe_utils.check_key_rows(rows, eta, ring_n, what)[source]¶
Raise unless every entry of rows (a key field’s name -> its rows, e.g. an encryption key’s
pkand padu) holds at least eta rows (a list, a tuple or a numpy array of rows) whose first eta are ring_n values each (is_values(): a list, a tuple or a 1-D numpy array, as keys converted to numpy encrypted correctly before this check). With fewer rows or rows of another length the encryption and key derivation zipped the key away: short ciphertexts, keys that decrypt to wrong values (on the Python path), or anIndexError/ValueErrordeep in a backend.
- pyfe4ai.primitives.lattice.ring_lwe_utils.check_ring_n(ring_n)[source]¶
Return ring_n if it is a power of two >= 2 (
x^n + 1must be the cyclotomic polynomial of BMMS21 and the NTT needs2n | q - 1).- Return type:
- pyfe4ai.primitives.lattice.ring_lwe_utils.RING_LWE_128_LOG_Q¶
For each error width
sigma(outer key) and ring dimensionn(inner key): the largestksuch that LWE in dimensionnwithq = 2^k, secret and errorsD_{Z, sigma}and unlimited samples costs at least 2^128 operations by the lattice estimator (commit 53da598, MATZOV cost model; primal uSVP, BDD and BDD hybrid, dual and dual hybrid). Ring-LWE is estimated as plain LWE, as in the Homomorphic Encryption Standard, whose table (ternary secret,sigma = 3.2) this estimator puts at about 125 bits. A scheme is held to the row of the largest width not above the smallest width among the RLWE problems its proof relies on: wider Gaussians only make the attacks more expensive. Regenerate withtools/params/estimate_ring_lwe.py.
- pyfe4ai.primitives.lattice.ring_lwe_utils.ring_lwe_bound(ring_n, sigma)[source]¶
Largest 128-bit
log2 qofRING_LWE_128_LOG_Qfor ring_n and the row of the largest tabulated width not above sigma, orNone.
- pyfe4ai.primitives.lattice.ring_lwe_utils.check_ring_lwe(ring_n, q, sigma)[source]¶
Raise unless Ring-LWE with
(ring_n, q)and width sigma meetsRING_LWE_128_LOG_Q.
- pyfe4ai.primitives.lattice.ring_lwe_utils.ring_lwe_auto_n(params_for)[source]¶
Smallest tabulated ring dimension
nwhoseparams_for(n) = (q, sigma)meetsRING_LWE_128_LOG_Q(the modulus grows with the dimension).- Return type:
- pyfe4ai.primitives.lattice.ring_lwe_utils.resolve_ring_n(ring_n, params_for)[source]¶
Ring dimension of a config entry:
"auto"selects the smallest one meetingRING_LWE_128_LOG_Q(seering_lwe_auto_n(); params_for maps a dimension to(q, rlwe_width)),NonegivesDEFAULT_RING_N.- Return type:
- pyfe4ai.primitives.lattice.ring_lwe_utils.bmms21_selective_parameters(sec_param, eta, bound_x, bound_y, ring_n, *, security_bound_x=None, ring_modulus='prime')[source]¶
Parameters of the selectively secure BMMS21 IPFE (Sec. 4) per Sec. 4.2.
For
l = etacoordinates bounded byX(messages) andY(weights),kappa = sec_paramandn = ring_n, witheta_eps(Z^d) = sqrt(ln(2 d (1 + 1/eps)) / pi):G1 -> G2 (mhe-RLWE from RLWE of width
sigma, Theorem 1):sigma = 1.5 eta_eps(Z^(n + n l))witheps = 2^-kappa(at least 3.2) andsigma2 = sqrt(2) sigma n C sqrt(l + 2),C = sqrt(kappa) sigma1(Lemma 2), so thatsigma sqrt(1 - (sigma n C sqrt(l + 2) / sigma2)^2) > eta;sigma3 = sqrt(2) sigma2;G3 -> G4 (Lemma 1 with
||alpha|| <= 2 X sqrt(l)):sigma1 = sqrt(2) 2 X sqrt(l) sigma', so thatGamma >= sigma' / sqrt(2), andsigma' = sqrt(2) eta_eps'(Z^n)witheps' = 2^-kappa / n; G5 -> G6 then needs RLWE with secrets and errors of widthsigma';correctness:
l Y (2 n kappa sigma1 sigma2 + sqrt(kappa) sigma3) < q / 2pwithp = 2 l X Y + 2(signed inner products|<x, y>| <= l X Ystrictly inside(-p/2, p/2));qis the first NTT-friendly prime above it.
security_bound_x(defaultbound_x) is the message boundXof the G3 -> G4 condition,bound_xthe one of decryption (p): the MIFE compiler of ACFGU18 (Fig. 4) needs an IPFE that is secure for messages of norm below3X(its Lemma 3 queries the IPFE withx^j - x^1 + x'^1), although onlyX-bounded results are decoded.ring_modulus="rns"replaces the prime by the product of two NTT primes above the same bound (rns_ntt_modulus()).Versions <= 0.2.0 used
sigma = 1andsigma1 = 2 sqrt(l) X(the paper’s Table 1 has the same gap), which miss both smoothing conditions.
- pyfe4ai.primitives.lattice.ring_lwe_utils.bmms21_parameters(sec_param, eta, bound_x, bound_y, ring_n, *, security_bound_x=None, ring_modulus='prime')[source]¶
(p, q, sigma1, sigma2, sigma3)ofbmms21_selective_parameters().
- pyfe4ai.primitives.lattice.ring_lwe_utils.BMMS21_ADAPTIVE_MIN_M = 5¶
Smallest number
mof key polynomials for which the constraints of BMMS21 Appendix C.1 can hold (seebmms21_adaptive_log2_q()).
- pyfe4ai.primitives.lattice.ring_lwe_utils.BMMS21_ADAPTIVE_MAX_M = 64¶
Largest
mthatm = "auto"considers.
- pyfe4ai.primitives.lattice.ring_lwe_utils.bmms21_adaptive_leveraging(eta, bound_x, ring_n, packed=False)[source]¶
(log2 L, D): the complexity-leveraging factorLof BMMS21 Sec. 5 and the rankDof the challenge differences that games G3* - G5* decompose.One vector per ciphertext (
packed=False):L = (2X + 1)^(2l), the challenge pairs, andD = 1. Up toring_nvectors per ciphertext (packed=True, Remark 1): the statistical games depend on the challenge only throughV = span(alpha^1, ..., alpha^k)of the differencesalpha^j = x1^j - x0^j(the keys they answer are those withyorthogonal toV), so guessingVsuffices.Vhas a dimensiond <= D = min(l, ring_n)and is spanned bydvectors of[-2X, 2X]^l, soL = (D + 1) (4X + 1)^(l D), independent of the number of packed vectors (guessing both challenge matrices would give(2X + 1)^(2lk)). Forl = 1,Vis{0}orQ:L = 2.
- pyfe4ai.primitives.lattice.ring_lwe_utils.bmms21_adaptive_log2_q(sec_param, eta, bound_x, bound_y, ring_n, m, packed=False)[source]¶
Smallest
log2 qmeeting the constraints of BMMS21 Appendix C.1, orNone.All widths grow like
q^(2/m)(the Ring-LHL bound onsigma') and the correctness noise likeq^(4/m), so a modulus exists only form > 4; the paper’sm = 3has none. packed: up toring_nvectors per ciphertext (seebmms21_adaptive_leveraging());qmust then also exceed twice the bound of_bmms21_packed_minor_log2(a largerqonly widens the correctness margin, since the noise grows likeq^(4/m)).
- pyfe4ai.primitives.lattice.ring_lwe_utils.bmms21_adaptive_parameters(sec_param, eta, bound_x, bound_y, ring_n, m, packed=False)[source]¶
Parameters of the adaptively secure BMMS21 IPFE (Sec. 5) for
mkey polynomials (packed: up toring_nvectors per ciphertext).- Return type:
- Returns:
Dict with
p(2 l X Y + 2),q(NTT-friendly prime),m, and the widthssigma(RLWE),sigma_lhl(sigma'),sigma1,sigma2,sigma3; see_bmms21_adaptive_widths.- Raises:
FEValidationError – If no modulus meets the constraints (
m <= 4).- Parameters:
- pyfe4ai.primitives.lattice.ring_lwe_utils.bmms21_adaptive_choice(sec_param, eta, bound_x, bound_y, ring_n, m, standard, packed=False)[source]¶
Ring dimension and
mof the adaptive BMMS21 IPFE, then its parameters.ring_n = "auto"tries the dimensions ofRING_LWE_128_LOG_Qin increasing order (NonegivesDEFAULT_RING_N);m = "auto"takes the smallestm >= 5that admits a modulus – one meetingRING_LWE_128_LOG_Qat the RLWE widthsigmawhen standard is set andring_normis"auto". A smallmkeeps thel mkey polynomials and them-polynomial functional keys small; a larger one lowersq(sigma'grows likeq^(2/m)) and can be passed explicitly. packed: parameters for up toring_nvectors per ciphertext.
Matrices and Digits mod q¶
Integer matrix/vector arithmetic under a modulus.
- pyfe4ai.primitives.lattice.matrix_utils.matmul_mod(left, right, modulus)[source]¶
Multiply two matrices modulo a given modulus.
- pyfe4ai.primitives.lattice.matrix_utils.matvec_mod(matrix, vector, modulus)[source]¶
Multiply a matrix by a vector modulo a given modulus.
- pyfe4ai.primitives.lattice.matrix_utils.vecdot_mod(left, right, modulus)[source]¶
Compute the dot product of two vectors modulo a given modulus.
Gaussian Sampling¶
Random sampling utilities: uniform vectors/matrices and discrete Gaussians.
- pyfe4ai.primitives.sampling.GAUSSIAN_SAMPLERS = ('rounded', 'facct')¶
The Gaussian samplers,
"rounded"(the library-wide default; a rounded continuous Gaussian) and"facct"(exactD_{Z,sigma}, seediscrete_gaussian_facct()). Select one per call (sampler=), per context (gaussian_sampler()), per process (set_gaussian_sampler()) or with thePYFE4AI_GAUSSIAN_SAMPLERenvironment variable. Exception: the schemes that record their sampler in their public parameters (seescheme_sampler()) default to"facct"and ignore the process-wide choice (ABDP15 only understandard), and the threshold flooding is pinned (FLOODING_SAMPLER).
- pyfe4ai.primitives.sampling.get_gaussian_sampler()[source]¶
Name of the Gaussian sampler in effect in this context.
- Return type:
- pyfe4ai.primitives.sampling.set_gaussian_sampler(name)[source]¶
Set the process-wide default Gaussian sampler (
"rounded"or"facct").Parties that derive noise from a shared seed (the flooding noise of the threshold schemes, drawn from an
XofRandom) must use the same sampler, or they derive different noise.
- pyfe4ai.primitives.sampling.gaussian_sampler(name)[source]¶
Use the Gaussian sampler name inside a
withblock.The choice is a context variable: threads started inside the block use the process-wide default.
- Parameters:
name (
str)
- pyfe4ai.primitives.sampling.PROOF_SAMPLER = 'facct'¶
the exact
D_{Z,sigma}the proofs of the Ring-LWE MIFE / MCFE family and of the dense-LWE MCFE keystassume (the crypto review of October 2026 found the double-precision rounded sampler 2^-25..2^-4 from it per key or ciphertext, against 2^-128 budgets). SIFE FullySec Ring-LWE has the same default (its ownDEFAULT_SAMPLER, passed toscheme_sampler()withuse_context=False), and so has the public keys’ noise of ABDP15 understandard(undertoyits default is the library-wide choice,get_gaussian_sampler()).- Type:
The default of
scheme_sampler()
- pyfe4ai.primitives.sampling.scheme_sampler(config, pp=None, default='facct', profile=None, use_context=True)[source]¶
The Gaussian sampler of a scheme object whose proofs need
D_{Z,sigma}.In this order:
config["gaussian_sampler"]; else that of its public parameters pp; else the one agaussian_sampler()block selects (not under thestandardprofile, nor withuse_context=False, as for SIFE FullySec Ring-LWE); else default. The process-wide choice (set_gaussian_sampler(),PYFE4AI_GAUSSIAN_SAMPLER) does not apply: these schemes record their sampler in their public parameters. profile is the key generator’ssecurity_profile(crypto objects take that of pp).- Raises:
FEValidationError – For an unknown sampler, and for an explicit
"rounded"under thestandardprofile (the double-precision rounded sampler does not meet its2^-128budgets).- Warns:
FESecurityWarning – When a crypto object’s config selects
"rounded"although its public parameters record"facct"(toy profile); attributed to the application’s call (warn_at_caller()).- Parameters:
- Return type:
- pyfe4ai.primitives.sampling.rand_uniform_matrix(rows, cols, modulus)[source]¶
Generate a random matrix with entries uniform in [0, modulus).
- pyfe4ai.primitives.sampling.rand_uniform_vector(length, modulus)[source]¶
Generate a random vector with entries uniform in [0, modulus).
- pyfe4ai.primitives.sampling.rand_bit_matrix(rows, cols)[source]¶
rows x colsuniform bits (uint8) from the OS CSPRNG.
- pyfe4ai.primitives.sampling.discrete_gaussian_matrix(rows, cols, sigma, sampler=None)[source]¶
Sample a matrix from the (rounded) discrete Gaussian distribution.
With
sampler="rounded", samples are Box-Muller normals in double precision from a CSPRNG (random.SystemRandom, i.e. OS entropy; the native kernels read their per-call DRBG) scaled by sigma and rounded to the nearest integer. Widths above2^40are sampled withdiscrete_gaussian_bigint()(a double would leave the low bits of every sample zero). Besides being a rounded continuous Gaussian, the double path has artifacts of its own: the two outputs of a pair share one radius (rings that show in the tail), exact half-integer ties round to even (about2^-53.8 sigmaper sample), and no value exceeds about8.57 sigma. Per the crypto review of October 2026 one ciphertext’s noise is then about 2^-12..2^-4 and a key 2^-25..2^-15 from i.i.d.D_{Z,sigma}: fine for toy parameters and benchmarks, not for the distributions of the security proofs (which is why the Ring-LWE MIFE / MCFE family, the dense-LWE MCFE keys, SIFE FullySec Ring-LWE and, understandard, the ABDP15 public keys default to"facct", seescheme_sampler(); ALS16, the DCR schemes and ABDP15 undertoyfollow the library-wide choice). It is not constant-time either. Withsampler="facct"the samples come fromdiscrete_gaussian_facct()(exactD_{Z,sigma}).
- class pyfe4ai.primitives.sampling.XofRandom(seed)[source]¶
Bases:
objectDeterministic bit source: SHAKE-256 keyed by a secret seed.
Provides
getrandbitsfor derandomised sampling (the same seed always yields the same bits). The seed must be secret and unpredictable. Callc(from 0) hasheslen(seed) || seed || c; each call claims itscatomically, so threads sharing an instance never draw the same block (which thread gets which block then depends on scheduling). While the native sampler draws from an instance (a number of calls known only afterwards), other threads’ draws from it wait for the end, and a draw from the same thread (a signal handler, a finalizer) raisesRuntimeError.- Parameters:
seed (
bytes) – Secret seed bytes (at least 32 bytes of entropy).
- pyfe4ai.primitives.sampling.BIGINT_GUARD_BITS = 320¶
its uniforms and arithmetic have
bits(sigma) + BIGINT_GUARD_BITSbits. 320 coversbits(sigma) + stat_param + ceil(log2 N) + 8for every statistical parameter the key generators accept (at mostMAX_STAT_PARAM= 256) and up to2^56samples per key: the dozen MPFR roundings of one sample stay below2^-(stat_param + log2 N + 8)in statistical distance from the ideal rounded Gaussian. (It was 128, which the October 2026 crypto review found 2 to 19 bits short of the2^-128budgets of the DCR keys and masks and of the threshold flooding; then 192, which covered stat_param <= 128 only.)- Type:
Guard bits of the MPFR precision of
discrete_gaussian_bigint()
- pyfe4ai.primitives.sampling.FLOODING_SAMPLER = 'rounded'¶
The sampler of the threshold flooding noise, whatever the process-wide or context choice: the MPFR rounded Gaussian (an ideal rounded Gaussian is a mixture of centred discrete Gaussians, which the flooding argument accepts). Recorded in the decryption keys as
noise_sampler(checked bycheck_flooding_version()), so that a server never answers one request with noises of two samplers.
- pyfe4ai.primitives.sampling.bigint_precision(sigma)[source]¶
Bits of the uniforms and of the MPFR arithmetic of
discrete_gaussian_bigint()for sigma:bits(ceil(sigma)) + 320.- Return type:
- pyfe4ai.primitives.sampling.discrete_gaussian_bigint(sigma, count, rng=None, sampler=None, precision=None)[source]¶
Sample count integers from a rounded Gaussian of arbitrary width.
Unlike
discrete_gaussian_matrix(), sigma may exceed the range of a double (e.g. thesqrt(kappa) * N^{5/2}widths of the DCR schemes): a Box-Muller sample is computed withgmpy2.mpfratbits(sigma) + 320bits of precision (BIGINT_GUARD_BITS) from CSPRNG uniforms and rounded to the nearest integer. As withdiscrete_gaussian_matrix()this is a rounded continuous Gaussian, not an exact discrete-Gaussian sampler, and it is not constant-time. Withsampler="facct"the samples come fromdiscrete_gaussian_facct()(exactD_{Z,sigma}) with the same rng.With the native backend (
pyfe4ai.backend) the rounded samples of widths below2^448are computed by the extension when rng is anXofRandomorNone: from the samegetrandbitsvalues, it returns the integers this function computes (seegaussian_bigint()); withrng=Noneits bits come from the extension’s per-call DRBG.- Parameters:
sigma – Standard deviation (int, mpz, float or mpfr), > 0.
count (
int) – Number of samples.rng (default:
None) – Bit source withgetrandbits(default: the OS CSPRNG). AnXofRandomgives derandomised sampling.sampler (
str|None, default:None) –"rounded"or"facct";Nonetakesget_gaussian_sampler().precision (
int|None, default:None) – Bits of the rounded sampler, at least (and by default)bigint_precision()= bits(sigma) + BIGINT_GUARD_BITS; FACCT has a fixed precision and ignores it.
- Return type:
- pyfe4ai.primitives.sampling.FACCT_PRECISION = 192¶
Bits of the CDT thresholds and of the acceptance tests of
discrete_gaussian_facct(); each sample is within statistical distance about2^-185ofD_{Z,sigma}.
- pyfe4ai.primitives.sampling.facct_base_width(sigma)[source]¶
(k, sigma / k): the double-sampling factor and the base width used bydiscrete_gaussian_facct()for sigma.
- pyfe4ai.primitives.sampling.discrete_gaussian_facct(sigma, count, rng=None)[source]¶
Sample count integers from the discrete Gaussian
D_{Z,sigma}.FACCT-style double sampling (Zhao, Steinfeld and Sakzad, “FACCT: FAst, Compact, and Constant-Time Discrete Gaussian Sampler over Integers”, IEEE Trans. Computers 2020, after the BLISS sampler of Ducas et al., CRYPTO 2013; GoFE’s
NormalDoubleConstanthas the same structure). Withk = ceil(sigma / sigma_0),sigma_0 = 1/sqrt(2 ln 2), and the base widthsigma_b = sigma / k <= sigma_0, one trial:u <- getrandbits(P);x= number of CDT thresholdsT_i <= u(the whole table is scanned), soxfollows the half-GaussianD_{Z>=0, sigma_b};yuniform in[0, k):getrandbits(bits(k - 1))until belowk(no bits whenk = 1);v <- getrandbits(P); acceptz = k x + yiffv < floor(2^P exp(-y (y + 2 k x) / (2 sigma^2))), so thatzfollowsD_{Z>=0, sigma};s <- getrandbits(1); ifz = 0ands = 1start over, else return(-1)^s z.
PisFACCT_PRECISION. Deviation from FACCT: its base is the fixedD_{Z>=0, sigma_0}, which only gives widthsk sigma_0; here the base table is computed forsigma / k(and cached), so every width is sampled exactly as requested. The tables and the acceptance thresholds use correctly rounded MPFR arithmetic: for a given bit source the samples are the same on every platform, which makes known-answer tests portable to other implementations (the order ofgetrandbitscalls above is part of that contract).The number of trials does not depend on the returned value, and no step branches on secret data except through the comparisons above. CPython and big-integer arithmetic still take data-dependent time, so this function is not constant-time; it is the reference for a native implementation that can be. It is also several times slower than the rounded sampler in Python.
With the native backend (
pyfe4ai.backend) the extension draws the samegetrandbitsin the same order and makes the same decisions when rng is anXofRandomorNone(gaussian_facct(); withrng=Noneits bits come from the per-call DRBG).
Hashing¶
Hashing to groups and to Z_q: random-oracle hashes into the quadratic residues
of Z_p^* and into Z_{N^2}^*, the ALS16 Paillier base, and the label hashes
(LABEL_HASHES) of the MCFE schemes.
- pyfe4ai.primitives.hashing.hash_to_qr_group(v, p, index=0, domain='')[source]¶
Hash a string into the subgroup of quadratic residues of
Z_p^*.For a safe prime
p = 2q + 1this is the prime-order-qsubgroup used by the DDH schemes. The digest is expanded with SHAKE-256 tobits(p) + 128bits (so the reduction modpis statistically close to uniform) and squared; the discrete logarithm of the result with respect to any fixed generator is unknown, as required when the hash is modelled as a random oracle into the group.
- pyfe4ai.primitives.hashing.paillier_base(seed, n_mod)[source]¶
The base
g = H(seed)^(2N) mod N^2of the ALS16 DCR schemes.A random
2N-th residue, as the scheme takes it; a parameter cache stores only the seed, so a planted cache cannot chooseg(e.g. of order 2, or1 + N), only another pseudo-random one.
- pyfe4ai.primitives.hashing.hash_to_zn2_star(v, n_mod, domain='')[source]¶
Full-domain hash of a string into
Z_{N^2}^*(random oracle).The digest is expanded to
bits(N^2) + 128bits, so the reduction modN^2is statistically close to uniform; outputs not coprime toN(which would factorN) are re-hashed with a counter.
- pyfe4ai.primitives.hashing.LABEL_HASHES = ('shake256-wide', 'shake256-rejection', 'shake128-rejection')¶
How
hash_to_zq_vector()maps a string toZ_q(thelabel_hashsetting of the MCFE schemes):"shake256-wide": one SHAKE-256 stream cut into blocks ofbits(q) + 128bits, each reduced mod q (statistical distance below2^-128per value); the only mode up to v0.3.0 (LEGACY_LABEL_HASH);"shake256-rejection"/"shake128-rejection": rejection sampling mod q (exactly uniform) from a SHAKE-256 / SHAKE128 stream, seezq_rejection_parameters();"shake128-rejection"is the MCFE key generators’ default (DEFAULT_LABEL_HASH).
- pyfe4ai.primitives.hashing.LEGACY_LABEL_HASH = 'shake256-wide'¶
The hash of versions <= 0.3.0, of
hash_to_zq_vector()without a mode (e.g. the seeded Ring-LWEa) and of MCFE public parameters and keys without alabel_hashfield.
- pyfe4ai.primitives.hashing.DEFAULT_LABEL_HASH = 'shake128-rejection'¶
The
label_hashthe MCFE key generators use unless configured.
- pyfe4ai.primitives.hashing.LABEL_HASH_CODES = {'shake128-rejection': 2, 'shake256-rejection': 1, 'shake256-wide': 0}¶
The native backend’s code of each label-hash mode.
- pyfe4ai.primitives.hashing.check_label_hash(mode)[source]¶
Return mode if it is one of
LABEL_HASHES.- Raises:
ValueError – Otherwise.
- Return type:
- pyfe4ai.primitives.hashing.hash_to_zq_material(v, domain='', mode='shake256-wide')[source]¶
The XOF input of
hash_to_zq_vector()(also hashed natively, seepyfe4ai.backend); the rejection modes use their own tag, so no two modes share an input.
- pyfe4ai.primitives.hashing.zq_rejection_parameters(q)[source]¶
(nb, limit)of the rejection modes ofhash_to_zq_vector(): candidates arenb = ceil(bits(q) / 8) + 1big-endian bytes of the stream, accepted belowlimit = floor(2^(8 nb) / q) q(more than1 - 2^-8of them) and reduced mod q.q | limit, so an accepted candidate is uniform mod q.- Return type:
- pyfe4ai.primitives.hashing.hash_to_zq_vector(v, q, length, domain='', mode='shake256-wide')[source]¶
Hash a string to a vector of length elements of
Z_q(random oracle).modeis one ofLABEL_HASHES."shake256-wide"(the default here,LEGACY_LABEL_HASH) cuts one SHAKE-256 output stream into length blocks ofbits(q) + 128bits, each reduced mod q, hence statistically close to uniform. Versions <= 0.2.0 made one SHAKE-256 call per coordinate, so their outputs (and the ciphertexts of the schemes that hash labels this way) differ. The rejection modes are exactly uniform and read fewer XOF bytes (zq_rejection_parameters()).
- pyfe4ai.primitives.hashing.hash_to_zq_values(material, q, length, mode='shake256-wide')[source]¶
The length values of
hash_to_zq_vector()for its XOF input material (hash_to_zq_material()with the same mode); the reference of the backend’shash_to_zq().
Groups¶
Groups of the DDH and DCR schemes: integer groups, ristretto255, modular arithmetic and discrete logarithms.
Integer Groups¶
Groups over the integers: safe primes, the prime-order subgroups of Z_p^* of the
DDH schemes and DCR moduli – generation, and the checks of parameters loaded
from the parameter cache.
- pyfe4ai.primitives.groups.integer_groups.generate_group_primes(bits)[source]¶
Generates two safe prime numbers with the restriction:p=2q+1.
- pyfe4ai.primitives.groups.integer_groups.RFC3526_PRIMES¶
Safe primes of the RFC 3526 MODP groups 14 and 15, by bit length:
p = 2^b - 2^(b-64) - 1 + 2^64 (floor(2^(b-130) pi) + c). Used instead of a fresh safe prime when one of these sizes is requested – generating a 3072-bit safe prime takes minutes, and at these sizes a fixed, published group gives nothing to precomputation.
- pyfe4ai.primitives.groups.integer_groups.group_generator_threshold_fe(bits, r=2)[source]¶
Generate a safe prime and generator for threshold FE.
- pyfe4ai.primitives.groups.integer_groups.is_dl_group(p, q, g, bits, cofactor=2)[source]¶
Whether
p = cofactor q + 1is a bits-bit prime,qis a prime of aboutbitsbits (at most the cofactor’s bits fewer) andggenerates the order-qsubgroup ofZ_p^*; at a size with an RFC 3526 prime,pmust be that prime (another safe prime could carry a trapdoor).Used to check discrete-log parameters loaded from the parameter cache.
- pyfe4ai.primitives.groups.integer_groups.is_dcr_modulus(n, prime_bits)[source]¶
Whether n can be a product of two distinct prime_bits-bit primes: odd, composite, not a square, without prime factors below 2^16, of
2 prime_bits - 1or2 prime_bitsbits.Used to check DCR moduli loaded from the parameter cache (their factorisation is never stored, so it cannot be checked further).
Modular Arithmetic¶
Modular arithmetic helpers for signed-exponent modular exponentiation.
- pyfe4ai.primitives.groups.modular_utils.pow_signed(base, exponent, modulus)[source]¶
Compute modular exponentiation with signed exponent support.
- pyfe4ai.primitives.groups.modular_utils.FIXED_BASE_WINDOW = 6¶
Digit size (bits) of the fixed-base method of
pow_signed_many().
- pyfe4ai.primitives.groups.modular_utils.FIXED_BASE_MIN_COUNT = 16¶
Fewer exponents than this are raised one by one with
pow_signed().
- pyfe4ai.primitives.groups.modular_utils.pow_signed_many(base, exponents, modulus)[source]¶
Return
[base^e mod modulus for e in exponents];emay be negative.For many exponents of one base (an MCFE2 encryption of a key contribution raises one label hash to every coordinate of the key) the powers
base^(2^(w i))are computed once; each exponentiation then multiplies the powers selected by its base-2^wdigits into2^w - 1buckets and combines them (Brickell, Gordon, McCurley, Wilson, EUROCRYPT 1992, with the bucket method), aboutbits(e) / w + 2^(w + 1)products instead ofbits(e)squarings. A negative exponent inverts the power of|e|. The results equal those ofpow_signed().
- pyfe4ai.primitives.groups.modular_utils.check_zn2_elements(values, n_square)[source]¶
Parse ciphertext elements of
Z_{N^2}and reject values outside(0, N^2).- Return type:
- pyfe4ai.primitives.groups.modular_utils.decode_paillier(acc, n_mod, bound)[source]¶
Return
muwithacc = (1 + N)^mu mod N^2, centred, orNone.Nonewhen acc is not of that form or|mu| > bound: a decryption of a crafted ciphertext must not hand out a key-dependent value (e.g.ct0 = 1 + Nturnsct0^{-k}into-k mod N).
Discrete-Log Solver¶
Bounded discrete logarithm solvers for integer and pairing groups.
Provides two strategies:
Dlog table (default): precompute a lookup table of size O(√n) once per process (memoised in memory) and reuse it for every solve. Tables are never read from disk: a cache file in the working directory would let anyone with write access to it run code (pickle) or silently corrupt decryption results, and rebuilding is as cheap as verifying a cached copy.
Baby-step giant-step (BSGS) (optional): compute baby-step and giant-step values on the fly without any disk cache. O(√n) time and space per invocation. Useful for one-off solves or when disk caching is undesirable.
- pyfe4ai.primitives.groups.dlog_solver.dlog_build_table(g_str, p_str, bound)[source]¶
Build a dlog lookup table for discrete log in
[-bound, bound].
- pyfe4ai.primitives.groups.dlog_solver.dlog_table_solve(value, g_str, p_str, bound, table, m, giant)[source]¶
Solve
g^x ≡ value (mod p)withx ∈ [-bound, bound].- Parameters:
value – Input value.
g_str (
str) – Group generator as a digit string.p_str (
str) – Prime modulus as a digit string.bound (
int) – Upper bound on absolute values.table (
dict) – Baby-step table{g^j: j}for0 <= j < m(keys as integers; tables keyed bydigits(g^j)are still read).m (
int) – Baby-step count (table size).giant (
mpz) – Giant stepg^-m mod p.
- Return type:
- pyfe4ai.primitives.groups.dlog_solver.load_or_build_dlog_table(filepath, g_str, p_str, bound)[source]¶
Return a dlog lookup table for
gmodpcovering[-bound, bound].The table is built in memory and memoised per process. filepath is kept for API compatibility and ignored: earlier versions loaded a pickle (and a JSON fallback) from that location without any integrity check, which allowed arbitrary code execution / wrong results via a planted file.
- pyfe4ai.primitives.groups.dlog_solver.bsgs_solve_int(value, g_str, p_str, bound)[source]¶
Solve
g^x ≡ value (mod p)withx ∈ [-bound, bound]via BSGS.Uses the standard two-pass approach (matching GoFE / CiFEr reference implementations): search
[0, bound]withg, then search[0, bound]withg⁻¹to cover negative solutions.Unlike the dlog-table approach, this computes baby-step and giant-step values entirely on the fly without reading or writing any disk cache. Suitable for one-off discrete-log recovery or when caching is undesirable.
Complexity: O(√bound) time and space.
- Parameters:
value – Target group element
g^x mod p.g_str (
str) – Generator as agmpy2.digits()string.p_str (
str) – Modulus as agmpy2.digits()string.bound (
int) – Search range[-bound, bound].
- Return type:
- Returns:
The discrete logarithm x.
- Raises:
ValueError – If no solution is found within the bound.
- pyfe4ai.primitives.groups.dlog_solver.bsgs_solve_pairing(target, g, h, pairing_group_param, bound)[source]¶
Solve
e(g, h)^x = targetin a pairing group via on-the-fly BSGS.Uses the standard approach (matching CiFEr’s
cfe_baby_giant_FP12): build one baby-step table fore(g,h)and simultaneously check bothtargetandtarget⁻¹in each giant step, covering[-bound, bound]in a single loop.Computes baby-step and giant-step values entirely in memory without persisting any lookup table.
- Parameters:
- Return type:
- Returns:
The discrete logarithm x.
- Raises:
ValueError – If no solution is found within the bound.
Elliptic-Curve Group (ristretto255)¶
Group layer of the elliptic-curve DDH schemes (ristretto255).
The */ddh_ristretto255 schemes run the DDH constructions of the library
(ABDP15, ACFGU18 Fig. 4, CDGPP18) in the prime-order group ristretto255
(RFC 9496) instead of the order-q subgroup of Z_p^*. This module holds
what they share: the serialisation of group elements (64 hex digits of the
canonical encoding) and scalars (decimal strings mod L), hashing labels
into the group (RFC 9380 hash_to_ristretto255), and the bounded discrete
logarithm of decryption (baby-step giant-step over encodings).
Arithmetic is the backend’s (pyfe4ai.backend.ops()): in the Rust
extension (curve25519-dalek, constant-time scalar multiplication) with group
elements held as their 32-byte encodings, or in
pyfe4ai.primitives.groups.ristretto255 (pure Python, not constant-time) on
extended coordinates. Every function accepts both forms.
- pyfe4ai.primitives.groups.ec_group.ORDER = 7237005577332262213973186563042994240857116359379907606001950938285454250989¶
Group order.
- pyfe4ai.primitives.groups.ec_group.MAX_DLOG_BOUND = 281474976710656¶
Largest bound of
dlog()(ValueErrorabove), as the extension’s.
- pyfe4ai.primitives.groups.ec_group.scalar_from(value, what='scalar')[source]¶
A scalar from its decimal string (or integer), reduced mod
L.
- pyfe4ai.primitives.groups.ec_group.from_hex(value, what='group element')[source]¶
The group element of a 64-hex-digit canonical encoding (exactly 64 hex digits for a string: no signs, spaces or other separators, as natively).
- Raises:
FEValidationError – If value is not the encoding of a group element.
- Parameters:
what (
str, default:'group element')
- pyfe4ai.primitives.groups.ec_group.dst(purpose)[source]¶
Domain-separation tag
PYFE4AI-<purpose>-V01-CS02-with-ristretto255_XMD:SHA-512_R255MAP_RO_.
- pyfe4ai.primitives.groups.ec_group.hash_to_group(message, tag, index=0)[source]¶
hash_to_ristretto255(I2OSP(index, 1) || message, tag)(RFC 9380).
- pyfe4ai.primitives.groups.ec_group.dlog(point, bound)[source]¶
The
xwithx G = pointand|x| <= bound, orNone.- Raises:
ValueError – If bound is negative or above
MAX_DLOG_BOUND.- Parameters:
bound (
int)- Return type:
The prime-order group ristretto255 (RFC 9496), reference implementation.
Group elements are equivalence classes of edwards25519 points, represented
here by extended coordinates (X, Y, Z, T) with x = X/Z, y = Y/Z,
x y = T/Z over GF(2^255 - 19), and encoded canonically in 32 bytes
(RFC 9496 Sec. 4.3). Scalars live in Z_L, L = 2^252 + 2774...493.
hash_to_group() is hash_to_ristretto255 of RFC 9380 (suite
ristretto255_XMD:SHA-512_R255MAP_RO_): expand_message_xmd with
SHA-512 to 64 bytes, then the one-way map of RFC 9496 Sec. 4.3.4.
Pure Python on Python integers: a reference, a fallback and a test oracle for the native backend, about a millisecond per scalar multiplication. It is not constant-time (branches and big-integer arithmetic depend on secret scalars), like the rest of this research prototype.
- pyfe4ai.primitives.groups.ristretto255.P = 57896044618658097711785492504343953926634992332820282019728792003956564819949¶
Field modulus
2^255 - 19.
- pyfe4ai.primitives.groups.ristretto255.L = 7237005577332262213973186563042994240857116359379907606001950938285454250989¶
Order of the ristretto255 group.
- pyfe4ai.primitives.groups.ristretto255.sqrt_ratio_m1(u, v)[source]¶
RFC 9496 SQRT_RATIO_M1:
(was_square, r)withr = sqrt(u/v)(non-negative) ifu/vis a square, elsesqrt(i u / v).
- pyfe4ai.primitives.groups.ristretto255.SQRT_AD_MINUS_ONE = 25063068953384623474111414158702152701244531502492656460079210482610430750235¶
sqrt(a d - 1)with the sign of RFC 9496 Sec. 4.1 (odd).
- pyfe4ai.primitives.groups.ristretto255.BASE = (15112221349535400772501151409588531511454012693041857206046113283949847762202, 46316835694926478169428394003475163141307993866256225615783033603165251855960, 1, 46827403850823179245072216630277197565144205554125654976674165829533817101731)¶
The generator of RFC 9496 (the edwards25519 base point).
- pyfe4ai.primitives.groups.ristretto255.add(p1, p2)[source]¶
Sum of two points (complete twisted-Edwards addition, a = -1).
- pyfe4ai.primitives.groups.ristretto255.mul(point, k)[source]¶
k * point(any integer k, reduced mod L), 4-bit fixed window.- Parameters:
k (
int)
- pyfe4ai.primitives.groups.ristretto255.mul_base(k)[source]¶
k * BASEwith a precomputed table ofj 16^i BASE.- Parameters:
k (
int)
- pyfe4ai.primitives.groups.ristretto255.equal(p1, p2)[source]¶
RFC 9496 Sec. 4.5: equality of the classes.
- Return type:
- pyfe4ai.primitives.groups.ristretto255.encode(point)[source]¶
Canonical 32-byte encoding (RFC 9496 Sec. 4.3.2).
- Return type:
- pyfe4ai.primitives.groups.ristretto255.decode(data)[source]¶
The point of a canonical encoding (RFC 9496 Sec. 4.3.1).
- Raises:
ValueError – If data is not the canonical encoding of a group element.
- Return type:
- pyfe4ai.primitives.groups.ristretto255.from_uniform_bytes(data)[source]¶
RFC 9496 Sec. 4.3.4: the group element of 64 uniform bytes.
- Return type:
- pyfe4ai.primitives.groups.ristretto255.expand_message_xmd(msg, dst, length)[source]¶
RFC 9380 Sec. 5.3.1 with SHA-512.
Pairings¶
Pairing groups: BLS12-381 from the native extension, other curves from charm-crypto.
Pairing Backend¶
Pairing groups for the pairing-based schemes.
PairingGroup(name) returns BLS12-381 from the Rust extension
(pyfe4ai.primitives.pairing.bls12_381, arkworks) for name == "BLS12-381", the
default, and charm-crypto’s group of that name otherwise (MNT224, BN254, …).
charm is optional when the Rust extension is built; it is the fallback for
BLS12-381 only in the sense that every other curve still comes from charm.
pair, multi_pair, multiexp and pow_many dispatch on their
arguments, and ZR, G1, G2, GT are charm’s type codes, which
both backends share.
- pyfe4ai.primitives.pairing.pairing_backend.DEFAULT_PAIRING_GROUP = 'BLS12-381'¶
Default pairing group of every pairing-based scheme (asymmetric, type 3).
- pyfe4ai.primitives.pairing.pairing_backend.NATIVE_PAIRING_GROUPS = ('BLS12-381',)¶
The curves the Rust extension implements.
- pyfe4ai.primitives.pairing.pairing_backend.is_native_group(name)[source]¶
Whether the group name (or group object) is served by the Rust extension.
- Return type:
- class pyfe4ai.primitives.pairing.pairing_backend.PairingGroup(name='BLS12-381', *args, **kwargs)[source]¶
Bases:
objectA pairing group by name: BLS12-381 natively, other curves from charm.
- pyfe4ai.primitives.pairing.pairing_backend.pair(a, b)[source]¶
The pairing e(a, b) on the backend the elements come from.
- pyfe4ai.primitives.pairing.pairing_backend.multi_pair(lhs, rhs)[source]¶
prod_i e(lhs[i], rhs[i]); one final exponentiation on BLS12-381.
- pyfe4ai.primitives.pairing.pairing_backend.multiexp(bases, exps, group=None)[source]¶
prod_i bases[i] ** exps[i] for elements of one group (G1, G2 or GT).
One multi-scalar multiplication on BLS12-381; on charm groups the loop of exponentiations, with the exponents (ints or ZR elements) reduced mod the order of group (without group they must be non-negative ints). The result is the same group element either way.
- Raises:
ValueError – If there are no bases, or not as many exponents as bases.
- pyfe4ai.primitives.pairing.pairing_backend.pow_many(base, exps, group=None)[source]¶
[base ** e for e in exps]; on BLS12-381 with one precomputation for base.Exponents as for
multiexp().- Return type:
- pyfe4ai.primitives.pairing.pairing_backend.deserialize_many(group, items)[source]¶
[group.deserialize(x) for x in items](str or bytes), in one call on BLS12-381.- Return type:
- pyfe4ai.primitives.pairing.pairing_backend.require_pairing_backend(name=None)[source]¶
Raise a clear error if no backend implements the pairing group name.
Without name, only checks that some pairing backend is present (the Rust extension or charm);
PairingGroup()checks the group itself.- Return type:
- pyfe4ai.primitives.pairing.pairing_backend.pairing_available(name='BLS12-381')[source]¶
Whether
PairingGroup()can build the group name.- Return type:
- pyfe4ai.primitives.pairing.pairing_backend.check_asymmetric(group)[source]¶
Reject symmetric (type-1) pairing groups such as SS512.
Every pairing-based scheme here assumes an asymmetric group (SXDH, or a generic asymmetric bilinear group); in a symmetric group
G1 = G2and DDH is easy. A group is symmetric iff it pairs twoG1elements.- Parameters:
group – A
PairingGroupor a group name such as"BLS12-381".- Raises:
FEValidationError – If the group is symmetric.
- Return type:
Pairing Utilities¶
Pairing-based helper functions shared across FH-IPE / FH-Multi-IPE / Quadratic schemes.
These were originally private helpers in sife/fh_ipe_pairing.py and
mife/fh_multi_ipe_pairing.py. They are extracted here so that
cross-package imports reference utils rather than reaching into
another scheme family’s private namespace.
- pyfe4ai.primitives.pairing.pairing_utils.mat_vec_mod(matrix, vector, modulus)[source]¶
Multiply a matrix by a vector modulo a given modulus.
- pyfe4ai.primitives.pairing.pairing_utils.vector_add_mod(lhs, rhs, modulus)[source]¶
Add two vectors element-wise modulo a given modulus.
- pyfe4ai.primitives.pairing.pairing_utils.vector_scalar_mod(vector, scalar, modulus)[source]¶
Multiply a vector by a scalar modulo a given modulus.
- pyfe4ai.primitives.pairing.pairing_utils.random_nonzero_zr(group)[source]¶
- Parameters:
group (
PairingGroup)
- pyfe4ai.primitives.pairing.pairing_utils.to_zr(group, value)[source]¶
Convert an integer to a ZR element in the pairing group.
- Parameters:
group (
PairingGroup) – Pairing group instance.
- pyfe4ai.primitives.pairing.pairing_utils.matrix_inverse_mod(matrix, modulus)[source]¶
Compute the modular inverse of a matrix via Gaussian elimination.
- pyfe4ai.primitives.pairing.pairing_utils.bounded_discrete_log_gt(group, base, target, bound, cache=True)[source]¶
Solve a bounded discrete logarithm in GT via baby-step giant-step.
- Parameters:
group (
PairingGroup) – Pairing group instance.base – Base for modular exponentiation.
target – Target group element.
bound (
int) – Upper bound on absolute values (at most 2^48).cache (
bool, default:True) – BLS12-381: keep the baby-step table for later calls with this base (Falsefor a base used once; seepyfe4ai.primitives.pairing.bls12_381.gt_dlog()).
- Return type:
- pyfe4ai.primitives.pairing.pairing_utils.serialize_matrix_g(group, matrix)[source]¶
Serialize a matrix of group elements to strings.
- Parameters:
group (
PairingGroup) – Pairing group instance.matrix – Input matrix.
- Return type:
BLS12-381¶
The pairing group BLS12-381 on the Rust backend, with charm’s interface.
PairingGroup and pair() behave as charm-crypto’s for the
operations the pairing-based schemes use, so the schemes run unchanged on
BLS12-381 (arkworks, in pyfe4ai._rust_native, through the backend’s
bls12_381() kernels): random(ZR | G1 | G2 |
GT), init(type, value), order(), serialize / deserialize
(b"<type>:<base64 of the compressed encoding>"; deserialisation accepts
only the canonical string – a type tag 0 to 3, canonical base64 of
exactly the element’s encoding – and checks the encoding and subgroup
membership), hash(msg, G1 | G2) (RFC 9380 hash_to_curve, suites
BLS12381G1_XMD:SHA-256_SSWU_RO_ and BLS12381G2_XMD:SHA-256_SSWU_RO_,
with the domain separation tags of HASH_DST), ismember,
groupType; elements
written multiplicatively (*, /, **), ZR elements with + - * /
** and int(). Products of pairings (multi_pair()) take one final
exponentiation; gt_dlog() is a bounded discrete logarithm in GT
(bounds up to MAX_DLOG_BOUND) with a baby-step table cached per base
and bound (a few tables at most, least recently used evicted), or built for
one call for a base used once. Products of powers
(msm()) are one multi-scalar multiplication, powers of one base
(pow_many()) share one table, and PairingGroup.deserialize_many()
decodes a list in one call.
BLS12-381 gives about 120-126 bits of security after the exTNFS attacks on
its 381-bit field (Barbulescu and Duquesne, J. Cryptol. 2019), below the
128-bit target of the standard profile, which therefore emits a
FESecurityWarning. Not constant-time.
- pyfe4ai.primitives.pairing.bls12_381.MAX_DLOG_BOUND = 281474976710656¶
Largest bound of
gt_dlog()(ValueErrorabove).
- pyfe4ai.primitives.pairing.bls12_381.HASH_DST = {1: b'PYFE4AI-V01-CS01-with-BLS12381G1_XMD:SHA-256_SSWU_RO_', 2: b'PYFE4AI-V01-CS01-with-BLS12381G2_XMD:SHA-256_SSWU_RO_'}¶
RFC 9380 domain separation tags of
PairingGroup.hash().
- class pyfe4ai.primitives.pairing.bls12_381.PairingGroup(name='BLS12-381', *args, **kwargs)[source]¶
Bases:
objectBLS12-381 with the methods of charm’s
PairingGroupthat the schemes use.- Parameters:
name (
str, default:'BLS12-381')
- pyfe4ai.primitives.pairing.bls12_381.multi_pair(lhs, rhs)[source]¶
prod_i e(lhs[i], rhs[i]) with one final exponentiation.
- pyfe4ai.primitives.pairing.bls12_381.msm(bases, scalars)[source]¶
prod_i bases[i] ** scalars[i] for elements of one group (G1, G2 or GT) and int or ZR scalars, as one multi-scalar multiplication.
- pyfe4ai.primitives.pairing.bls12_381.pow_many(base, scalars)[source]¶
[base ** k for k in scalars]with one precomputation for base.
- pyfe4ai.primitives.pairing.bls12_381.gt_dlog(base, target, bound, cache=True)[source]¶
The
xwithbase^x = targetand|x| <= bound, or None.With cache the baby-step table (about
16 sqrt(2 bound + 1)steps) is kept for later calls with this base and bound; passcache=Falsefor a base used once (a table of aboutsqrt(2 bound + 1)steps, not kept).- Raises:
ValueError – If bound is negative or above
MAX_DLOG_BOUND.- Parameters: