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.

Parameters:
  • max_plain_bound (mpz) – Maximum plaintext bound.

  • bound_y (mpz) – Bound on weight values.

  • eta (int) – Inner-product vector dimension.

  • lwe_n (int) – LWE lattice dimension.

  • extra_dimension (int | None, default: None) – Extra dimension flag.

Return type:

tuple[mpz, mpz, int, float, mpz]

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.

Parameters:
  • bound_x (mpz) – Bound on plaintext values.

  • bound_y (mpz) – Bound on weight values.

  • eta (int) – Inner-product vector dimension.

  • lwe_n (int) – LWE lattice dimension.

Return type:

tuple[mpz, mpz, int, float, mpz, float, mpz, float, mpz]

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 used trunc(x q / p), which differs from it by a small non-linear term).

Parameters:
  • vector (list[int] | list[mpz]) – Input vector.

  • p (mpz) – Plaintext modulus.

  • q (mpz) – Ciphertext modulus.

Return type:

list[mpz]

pyfe4ai.primitives.lattice.lwe_utils.decode_lwe_inner_product(value, p, q)[source]

Extract a plaintext value from an LWE inner-product ciphertext.

Parameters:
  • value (mpz) – Input value.

  • p (mpz) – Prime modulus.

  • q (mpz) – Modulus (or second prime).

Return type:

int

pyfe4ai.primitives.lattice.lwe_utils.DENSE_LWE_128_LOG_Q

Largest q.bit_length() for which LWE in dimension n with a uniform secret modulo q, errors D_{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 the sigma_e of abmpsw20_lwe_parameters(), so this bounds the LWE problem of the ABMPSW20 security proof. Regenerate with tools/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)).

Parameters:

lwe_n (int)

Return type:

None

pyfe4ai.primitives.lattice.lwe_utils.dense_lwe_n(modulus_for)[source]

Smallest tabulated dimension whose modulus modulus_for(n) meets DENSE_LWE_128_LOG_Q (the modulus grows with the dimension).

Return type:

int

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 meeting DENSE_LWE_128_LOG_Q (see dense_lwe_n()), None gives default.

Parameters:

default (int)

Return type:

int

pyfe4ai.primitives.lattice.lwe_utils.WIDE_LWE_128_LOG_RATIO

For each error width 2^s (outer key) and LWE dimension n (inner key): the largest log2(q / sigma) for which LWE with a uniform secret, errors D_{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 key A S + E has very wide errors (sigma of 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 width sigma' 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 with tools/params/estimate_wide_lwe.py.

pyfe4ai.primitives.lattice.lwe_utils.wide_lwe_bound(lwe_n, sigma)[source]

Largest 128-bit log2(q / sigma) of WIDE_LWE_128_LOG_RATIO for lwe_n and sigma (rows at or below both), or None below the table.

Parameters:

lwe_n (int)

Return type:

int | None

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'_i for the difference d = x1 - x0 of 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, sufficient sigma >= (l B + 1) sigma'. The scheme is then as hard as LWE with errors of width sigma', not sigma.

Notes on how the library meets the paper’s hypotheses:

  • The literal p > l B^2 of Thm. 4.4 / Sec. 6.2 is not needed for the Regev instance. The change of basis acts on secret keys mod the LWE prime q, and decoding the integer inner product (p > 2 eta X Y + 1) makes admissible keys orthogonal to d over Z.

  • The thesis’s LWE hardness bound (an absolute width above 2 sqrt(n)), which the full version does not state, is met under standard: sigma' >= 2^16, the table’s smallest width.

  • Theorem 2 of the proceedings version does not cover noisy LWE keys.

Parameters:

eta (int)

Return type:

float

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.

Parameters:
  • lwe_n (int)

  • what (str, default: 'sigma')

Return type:

None

pyfe4ai.primitives.lattice.lwe_utils.wide_lwe_n(params_for, lwe_sigma)[source]

params_for(n) (a dict with q and sigma_q) for the smallest tabulated n that meets WIDE_LWE_128_LOG_RATIO at the width lwe_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:

dict

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 meeting WIDE_LWE_128_LOG_RATIO at the width lwe_sigma(params) (see wide_lwe_n(); the key generators pass the width of their security reduction, abdp15_reduction_sigma()).

Return type:

dict

Returns:

Dict with lwe_n, p, q, m, sigma_q and l_sigma.

Raises:

ValueError – For lwe_n = "auto" without lwe_sigma.

Parameters:
  • eta (int)

  • extra_dimension (int | None, default: None)

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| <= X and |y| <= Y; the inner product then lies in [-T, T] with T = n X Y and is encoded with K = 2T + 1 and decoded centred. The constraints of Appendix C.2/C.3 are then instantiated as:

  • correctness, q0 > K (nV + 1): q0 is 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^2 with P = 2X + 1 bounding ||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 cut c and n0^{omega(1)} instantiated as 2^stat_param; q is 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).

m0 and q depend on each other and are iterated to a fixed point. In the adaptive setting m0 grows linearly in n log2 P (about 4,600 for 100 slots with X = Y = 100, kappa = 128), and every label costs n0 + m0 hash outputs and inner-product terms, so large ndarray workloads (one label per element) become slow. The LWE dimension n0 is taken as given; DENSE_LWE_128_LOG_Q bounds q for 128-bit hardness of LWE(n0, q, sigma_e).

Parameters:
  • n_slots (int) – Total number of slots n (clients x coordinates).

  • bound_x (int) – Plaintext bound X.

  • bound_y (int) – Weight bound Y.

  • lwe_n (int) – LWE secret dimension n0.

  • stat_param (int) – Statistical security parameter kappa.

  • security (str, default: 'selective') – "selective" or "adaptive" (choice of m0).

Return type:

dict

Returns:

Dict with integer entries q, q0, K, delta (floor(q / K)), bound_ip (T), n0, m0, sigma_t, sigma_e and B.

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 q first).

  • q – Large modulus.

  • q0 – Small modulus.

Return type:

mpz

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 signed mu.

Returns the integer mu closest to mu' q / (q0 delta) after centring mu' modulo q0, or None if |mu| > bound_ip.

Parameters:
  • mu_prime – Value modulo q0.

  • q – Large modulus.

  • q0 – Small modulus.

  • delta – Encoding factor floor(q / K).

  • bound_ip – Bound T on |mu|.

Return type:

int | None

pyfe4ai.primitives.lattice.lwe_utils.decode_scaled(value, delta, q, bound)[source]

Decode value ~ delta * mu + noise (mod q) to the signed integer mu.

Used by the two-step decryption of the LWE IPFEs (ACFGU18 Section 4.2, encoding E(gamma, noise) = gamma * delta + noise mod q).

Parameters:
  • value – Value modulo q.

  • delta – Scaling factor floor(q / p).

  • q – Modulus.

  • bound – Bound on |mu|; larger results return None.

Return type:

int | None

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 than MAX_ROWS = 2^22 limbs stays below 2^53 and is exact in double precision, so r^T A for a binary r (encryption) is one BLAS product per limb;

  • A S mod q (key generation) multiplies 16-bit pieces of A and S, whose products summed over fewer than 2^21 columns also stay below 2^53.

matrix_mode trades memory for time:

"memory" A is expanded once and kept, 8 L bytes per entry (about

2.4 GB for n = 1024, m = 96000 and a 92-bit q); an encryption is one pass over it (about 0.1 s at that size).

"stream" A is 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 for A S.

pyfe4ai.primitives.lattice.lwe_matrix.limb_count(q)[source]

Number of 31-bit limbs of an integer below 2^bits(q).

Return type:

int

pyfe4ai.primitives.lattice.lwe_matrix.check_matrix_mode(mode)[source]

Return mode if it is one of MATRIX_MODES.

Parameters:

mode (str)

Return type:

str

pyfe4ai.primitives.lattice.lwe_matrix.physical_memory_bytes()[source]

Physical memory of this machine, or None where the OS does not say.

Return type:

int | None

class pyfe4ai.primitives.lattice.lwe_matrix.LimbMatrix(entries, q)[source]

Bases: object

An explicit matrix over Z_q (e.g. a public key A S + E) held as limbs.

binary_combinations(r)[source]

[r_b^T M mod q]_b for the rows r_b of a 0/1 matrix r.

Return type:

list

class pyfe4ai.primitives.lattice.lwe_matrix.SeededMatrix(seed, rows, cols, q, mode='memory', chunk_rows=4096)[source]

Bases: object

Uniform A in Z_q^{rows x cols} expanded from a hex seed (see the module docstring).

Parameters:
  • seed (str) – Hex string (the public seed).

  • rows (int) – Shape of A.

  • cols (int) – Shape of A.

  • q – Modulus.

  • mode (str, default: 'memory') – "memory" (expand once, keep) or "stream" (expand per product).

  • chunk_rows (int, default: 4096) – Rows per chunk in "stream" mode and for times().

classmethod shared(seed, rows, cols, q, mode='memory')[source]

One instance per (seed, shape, q, mode) and process, so that several encryptors in one process (e.g. simulated clients) expand A once.

Parameters:
  • seed (str)

  • rows (int)

  • cols (int)

  • mode (str, default: 'memory')

Return type:

SeededMatrix

binary_combinations(r)[source]

[r_b^T A mod q]_b for the rows r_b of a 0/1 matrix r (B x rows).

Return type:

list

times(s)[source]

A S mod q (rows x k) for an integer matrix s (cols x k).

Return type:

list

entries()[source]

All entries as Python integers (for tests and small matrices).

Return type:

list

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.

Parameters:
Return type:

list[mpz]

pyfe4ai.primitives.lattice.ring_lwe_utils.ntt_inverse(a, q, root)[source]

Inverse NTT: transform back to coefficient domain.

The backend’s ntt() (natively as ntt_forward()), else _ntt_inverse_reference.

Args:

a: First polynomial (coefficient vector). q: Modulus (or second prime). root: Primitive root of unity for NTT.

Parameters:
Return type:

list[mpz]

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.

Parameters:
  • a (list[mpz]) – First polynomial (coefficient vector).

  • b (list[mpz]) – Second polynomial (coefficient vector).

  • q (mpz) – Modulus (or second prime).

  • root (mpz) – Primitive root of unity for NTT.

Return type:

list[mpz]

pyfe4ai.primitives.lattice.ring_lwe_utils.RING_MODULI = ('prime', 'rns')

ring_modulus of 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:

str

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 composite q has no NTT here.

Return type:

tuple

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()), or None.

Return type:

tuple | None

pyfe4ai.primitives.lattice.ring_lwe_utils.prev_ntt_prime(upper_bound, ring_n)[source]

The largest prime q <= upper_bound with q = 1 (mod 2 ring_n), or None.

Parameters:

ring_n (int)

Return type:

mpz | None

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 of k = factors distinct NTT primes, in increasing order.

k - 1 factors are the largest NTT primes <= floor(lower_bound^(1/k)) (descending), the last the smallest NTT prime with a product >= lower_bound that is not one of them, advanced while gcd(floor(q / p), q) != 1 (Delta is then a unit, as for a prime q; see the review). For k = 2: q1 the largest NTT prime <= sqrt(lower_bound), q2 the smallest with q1 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_BITS bits or the bound is too small for factors factors.

Parameters:
  • ring_n (int)

  • factors (int, default: 2)

Return type:

tuple

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) – but RNS_NATIVE_FACTORS (four, of up to RNS_MAX_FACTOR_BITS bits) up to 248 bits, the most the native ring holds, rather than five.

Parameters:

min_factors (int, default: 2)

Return type:

int

pyfe4ai.primitives.lattice.ring_lwe_utils.rns_ntt_modulus_for(lower_bound, ring_n, p, min_factors=2)[source]

rns_ntt_modulus() with rns_factor_count() factors (at least min_factors), or one more where the last factor would exceed RNS_MAX_FACTOR_BITS bits (bounds just below 2^248 with four factors). Deterministic.

Raises:

FEValidationError – As rns_ntt_modulus() for both counts.

Parameters:
  • ring_n (int)

  • min_factors (int, default: 2)

Return type:

tuple

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).

Parameters:
Return type:

list[mpz]

class pyfe4ai.primitives.lattice.ring_lwe_utils.RingNTT(q, ring_n, root)[source]

Bases: object

NTT-domain arithmetic of Z_q[x] / (x^n + 1) with poly_mul()’s root.

inverse(mul(forward(a), forward(b))) equals poly_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 the poly_mul() products mod q. get() returns None when q has no primitive 2n-th root of unity (then poly_mul() falls back to the naive product).

Parameters:

ring_n (int)

classmethod get(q, ring_n)[source]
Parameters:

ring_n (int)

Return type:

RingNTT | None

forward(a)[source]

NTT of the coefficient vector a (integers, any sign).

Parameters:

a (list)

Return type:

list

inverse(a_hat)[source]

Coefficients, in [0, q), of the NTT-domain vector a_hat.

Parameters:

a_hat (list)

Return type:

list

mul(a_hat, b_hat)[source]

Pointwise product mod q (the NTT of the negacyclic product).

Parameters:
Return type:

list

mul_add(acc, a_hat, b_hat)[source]

acc + a_hat * b_hat pointwise mod q (acc = None: just the product).

Parameters:
Return type:

list

pyfe4ai.primitives.lattice.ring_lwe_utils.next_ntt_prime(lower_bound, ring_n)[source]

Find the smallest prime q >= lower_bound with q ≡ 1 (mod 2·ring_n).

Parameters:
  • lower_bound (mpz) – Lower bound for the search.

  • ring_n (int) – Polynomial ring dimension.

Return type:

mpz

pyfe4ai.primitives.lattice.ring_lwe_utils.poly_add(a, b, q)[source]

Add two polynomials element-wise modulo q.

Parameters:
  • a (list[mpz]) – First polynomial (coefficient vector).

  • b (list[mpz]) – Second polynomial (coefficient vector).

  • q (mpz) – Modulus (or second prime).

Return type:

list[mpz]

pyfe4ai.primitives.lattice.ring_lwe_utils.poly_neg(a, q)[source]

Negate a polynomial modulo q.

Parameters:
  • a (list[mpz]) – First polynomial (coefficient vector).

  • q (mpz) – Modulus (or second prime).

Return type:

list[mpz]

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).

Parameters:
  • a (list[mpz]) – First polynomial (coefficient vector).

  • b (list[mpz]) – Second polynomial (coefficient vector).

  • q (mpz) – Modulus (or second prime).

Return type:

list[mpz]

pyfe4ai.primitives.lattice.ring_lwe_utils.matrix_check_bound(matrix, bound)[source]

Check whether all matrix entries have absolute value within the bound.

Parameters:
  • matrix (list[list[int]]) – Input matrix.

  • bound (mpz) – Upper bound on absolute values.

Return type:

bool

pyfe4ai.primitives.lattice.ring_lwe_utils.transpose(matrix)[source]
Parameters:

matrix (list[list[int | mpz]])

Return type:

list[list[mpz]]

pyfe4ai.primitives.lattice.ring_lwe_utils.mat_vec_mul(matrix, vec, q)[source]

Multiply a matrix by a polynomial vector modulo q.

Parameters:
  • matrix (list[list[mpz]]) – Input matrix.

  • vec (list[mpz]) – Input vector.

  • q (mpz) – Modulus (or second prime).

Return type:

list[mpz]

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 i becomes a polynomial whose first coefficients hold the entries of the row. The encoding is linear in x, as the BMMS21 proofs need (versions <= 0.2.0 used trunc(x q / p)).

Parameters:
  • matrix (list[list[int]]) – Input matrix.

  • p (mpz) – Plaintext modulus.

  • q (mpz) – Ciphertext modulus.

  • ring_n (int) – Polynomial ring dimension.

Return type:

list[list[mpz]]

pyfe4ai.primitives.lattice.ring_lwe_utils.seeded_polynomial(seed, q, ring_n, index=0)[source]

Uniform element of R_q expanded from a hex seed with SHAKE-256.

The Ring-LWE schemes publish their public polynomial a this way: a parameter cache stores only the seed, so a planted cache cannot choose a (e.g. a = 0), only another pseudo-random one.

Parameters:
  • seed (str)

  • ring_n (int)

  • index (int, default: 0)

Return type:

list[mpz]

pyfe4ai.primitives.lattice.ring_lwe_utils.decode_vector(poly, p, q, k)[source]

Extract the first k plaintext coefficients by rounding.

Parameters:
  • poly (list[mpz]) – Input polynomial coefficient vector.

  • p (mpz) – Prime modulus.

  • q (mpz) – Modulus (or second prime).

  • k (int) – Inner-product bound.

Return type:

list[int]

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_n values, a polynomial): a list, a tuple or a 1-D numpy array.

Return type:

bool

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 pk and pad u) 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 an IndexError / ValueError deep in a backend.

Parameters:
  • rows (dict) – {field name: rows}.

  • eta (int) – Rows a key field must have.

  • ring_n (int) – Values per row.

  • what (str) – The fields as named in the message ("pk or u"); constant.

Raises:

FEKeyError – With a constant message (no key data).

Return type:

None

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 + 1 must be the cyclotomic polynomial of BMMS21 and the NTT needs 2n | q - 1).

Return type:

int

pyfe4ai.primitives.lattice.ring_lwe_utils.RING_LWE_128_LOG_Q

For each error width sigma (outer key) and ring dimension n (inner key): the largest k such that LWE in dimension n with q = 2^k, secret and errors D_{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 with tools/params/estimate_ring_lwe.py.

pyfe4ai.primitives.lattice.ring_lwe_utils.ring_lwe_bound(ring_n, sigma)[source]

Largest 128-bit log2 q of RING_LWE_128_LOG_Q for ring_n and the row of the largest tabulated width not above sigma, or None.

Parameters:

ring_n (int)

Return type:

int | None

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 meets RING_LWE_128_LOG_Q.

Parameters:

ring_n (int)

Return type:

None

pyfe4ai.primitives.lattice.ring_lwe_utils.ring_lwe_auto_n(params_for)[source]

Smallest tabulated ring dimension n whose params_for(n) = (q, sigma) meets RING_LWE_128_LOG_Q (the modulus grows with the dimension).

Return type:

int

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 meeting RING_LWE_128_LOG_Q (see ring_lwe_auto_n(); params_for maps a dimension to (q, rlwe_width)), None gives DEFAULT_RING_N.

Return type:

int

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 = eta coordinates bounded by X (messages) and Y (weights), kappa = sec_param and n = ring_n, with eta_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)) with eps = 2^-kappa (at least 3.2) and sigma2 = sqrt(2) sigma n C sqrt(l + 2), C = sqrt(kappa) sigma1 (Lemma 2), so that sigma 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 that Gamma >= sigma' / sqrt(2), and sigma' = sqrt(2) eta_eps'(Z^n) with eps' = 2^-kappa / n; G5 -> G6 then needs RLWE with secrets and errors of width sigma';

  • correctness: l Y (2 n kappa sigma1 sigma2 + sqrt(kappa) sigma3) < q / 2p with p = 2 l X Y + 2 (signed inner products |<x, y>| <= l X Y strictly inside (-p/2, p/2)); q is the first NTT-friendly prime above it.

security_bound_x (default bound_x) is the message bound X of the G3 -> G4 condition, bound_x the one of decryption (p): the MIFE compiler of ACFGU18 (Fig. 4) needs an IPFE that is secure for messages of norm below 3X (its Lemma 3 queries the IPFE with x^j - x^1 + x'^1), although only X-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 = 1 and sigma1 = 2 sqrt(l) X (the paper’s Table 1 has the same gap), which miss both smoothing conditions.

Return type:

dict

Returns:

Dict with p, q, sigma, sigma_prime, sigma1, sigma2, sigma3 and rlwe_width (min(sigma, sigma'), the width the 128-bit table is read at); with "rns" also q_factors.

Parameters:
  • sec_param (int)

  • eta (int)

  • ring_n (int)

  • ring_modulus (str, default: 'prime')

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) of bmms21_selective_parameters().

Parameters:
  • sec_param (int)

  • eta (int)

  • ring_n (int)

  • ring_modulus (str, default: 'prime')

Return type:

tuple

pyfe4ai.primitives.lattice.ring_lwe_utils.BMMS21_ADAPTIVE_MIN_M = 5

Smallest number m of key polynomials for which the constraints of BMMS21 Appendix C.1 can hold (see bmms21_adaptive_log2_q()).

pyfe4ai.primitives.lattice.ring_lwe_utils.BMMS21_ADAPTIVE_MAX_M = 64

Largest m that m = "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 factor L of BMMS21 Sec. 5 and the rank D of the challenge differences that games G3* - G5* decompose.

One vector per ciphertext (packed=False): L = (2X + 1)^(2l), the challenge pairs, and D = 1. Up to ring_n vectors per ciphertext (packed=True, Remark 1): the statistical games depend on the challenge only through V = span(alpha^1, ..., alpha^k) of the differences alpha^j = x1^j - x0^j (the keys they answer are those with y orthogonal to V), so guessing V suffices. V has a dimension d <= D = min(l, ring_n) and is spanned by d vectors of [-2X, 2X]^l, so L = (D + 1) (4X + 1)^(l D), independent of the number of packed vectors (guessing both challenge matrices would give (2X + 1)^(2lk)). For l = 1, V is {0} or Q: L = 2.

Parameters:
  • eta (int)

  • ring_n (int)

  • packed (bool, default: False)

Return type:

tuple

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 q meeting the constraints of BMMS21 Appendix C.1, or None.

All widths grow like q^(2/m) (the Ring-LHL bound on sigma') and the correctness noise like q^(4/m), so a modulus exists only for m > 4; the paper’s m = 3 has none. packed: up to ring_n vectors per ciphertext (see bmms21_adaptive_leveraging()); q must then also exceed twice the bound of _bmms21_packed_minor_log2 (a larger q only widens the correctness margin, since the noise grows like q^(4/m)).

Parameters:
Return type:

float | None

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 m key polynomials (packed: up to ring_n vectors per ciphertext).

Return type:

dict

Returns:

Dict with p (2 l X Y + 2), q (NTT-friendly prime), m, and the widths sigma (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 m of the adaptive BMMS21 IPFE, then its parameters.

ring_n = "auto" tries the dimensions of RING_LWE_128_LOG_Q in increasing order (None gives DEFAULT_RING_N); m = "auto" takes the smallest m >= 5 that admits a modulus – one meeting RING_LWE_128_LOG_Q at the RLWE width sigma when standard is set and ring_n or m is "auto". A small m keeps the l m key polynomials and the m-polynomial functional keys small; a larger one lowers q (sigma' grows like q^(2/m)) and can be passed explicitly. packed: parameters for up to ring_n vectors per ciphertext.

Parameters:
  • sec_param (int)

  • eta (int)

  • standard (bool)

  • packed (bool, default: False)

Return type:

dict

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.

Parameters:
  • left (list[list[mpz]]) – Left operand matrix or vector.

  • right (list[list[mpz]]) – Right operand matrix or vector.

  • modulus (mpz) – Modulus for the arithmetic operation.

Return type:

list[list[mpz]]

pyfe4ai.primitives.lattice.matrix_utils.transpose(matrix)[source]
Parameters:

matrix (list[list[mpz]])

Return type:

list[list[mpz]]

pyfe4ai.primitives.lattice.matrix_utils.matvec_mod(matrix, vector, modulus)[source]

Multiply a matrix by a vector modulo a given modulus.

Parameters:
  • matrix (list[list[mpz]]) – Input matrix.

  • vector (list[mpz]) – Input vector.

  • modulus (mpz) – Modulus for the arithmetic operation.

Return type:

list[mpz]

pyfe4ai.primitives.lattice.matrix_utils.vecdot_mod(left, right, modulus)[source]

Compute the dot product of two vectors modulo a given modulus.

Parameters:
  • left (list[mpz]) – Left operand matrix or vector.

  • right (list[mpz]) – Right operand matrix or vector.

  • modulus (mpz) – Modulus for the arithmetic operation.

Return type:

mpz

pyfe4ai.primitives.lattice.matrix_utils.matrix_to_digits(matrix)[source]
Parameters:

matrix (list[list[mpz]])

Return type:

list[list[str]]

pyfe4ai.primitives.lattice.matrix_utils.digits_to_matrix(matrix)[source]
Parameters:

matrix (list[list[str]])

Return type:

list[list[mpz]]

pyfe4ai.primitives.lattice.matrix_utils.vector_to_digits(vector)[source]
Parameters:

vector (list[mpz])

Return type:

list[str]

pyfe4ai.primitives.lattice.matrix_utils.digits_to_vector(vector)[source]
Parameters:

vector (list[str])

Return type:

list[mpz]

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" (exact D_{Z,sigma}, see discrete_gaussian_facct()). Select one per call (sampler=), per context (gaussian_sampler()), per process (set_gaussian_sampler()) or with the PYFE4AI_GAUSSIAN_SAMPLER environment variable. Exception: the schemes that record their sampler in their public parameters (see scheme_sampler()) default to "facct" and ignore the process-wide choice (ABDP15 only under standard), 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:

str

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.

Parameters:

name (str)

Return type:

None

pyfe4ai.primitives.sampling.gaussian_sampler(name)[source]

Use the Gaussian sampler name inside a with block.

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 keys t assume (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 own DEFAULT_SAMPLER, passed to scheme_sampler() with use_context=False), and so has the public keys’ noise of ABDP15 under standard (under toy its 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 a gaussian_sampler() block selects (not under the standard profile, nor with use_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’s security_profile (crypto objects take that of pp).

Raises:

FEValidationError – For an unknown sampler, and for an explicit "rounded" under the standard profile (the double-precision rounded sampler does not meet its 2^-128 budgets).

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:
  • config (dict | None)

  • pp (dict | None, default: None)

  • default (str, default: 'facct')

  • profile (str | None, default: None)

  • use_context (bool, default: True)

Return type:

str

pyfe4ai.primitives.sampling.random_below(maximum)[source]
Parameters:

maximum (mpz)

Return type:

mpz

pyfe4ai.primitives.sampling.rand_uniform_matrix(rows, cols, modulus)[source]

Generate a random matrix with entries uniform in [0, modulus).

Parameters:
  • rows (int) – Number of rows.

  • cols (int) – Number of columns.

  • modulus (mpz) – Modulus for the arithmetic operation.

Return type:

list[list[mpz]]

pyfe4ai.primitives.sampling.rand_uniform_vector(length, modulus)[source]

Generate a random vector with entries uniform in [0, modulus).

Parameters:
  • length (int) – Length of the vector.

  • modulus (mpz) – Modulus for the arithmetic operation.

Return type:

list[mpz]

pyfe4ai.primitives.sampling.rand_bit_vector(length)[source]
Parameters:

length (int)

Return type:

list[mpz]

pyfe4ai.primitives.sampling.rand_bit_matrix(rows, cols)[source]

rows x cols uniform bits (uint8) from the OS CSPRNG.

Parameters:
Return type:

ndarray

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 above 2^40 are sampled with discrete_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 (about 2^-53.8 sigma per sample), and no value exceeds about 8.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, under standard, the ABDP15 public keys default to "facct", see scheme_sampler(); ALS16, the DCR schemes and ABDP15 under toy follow the library-wide choice). It is not constant-time either. With sampler="facct" the samples come from discrete_gaussian_facct() (exact D_{Z,sigma}).

Parameters:
  • rows (int) – Number of rows.

  • cols (int) – Number of columns.

  • sigma (float) – Standard deviation for the Gaussian distribution.

  • sampler (str | None, default: None) – "rounded" or "facct"; None takes get_gaussian_sampler().

Return type:

list[list[mpz]]

class pyfe4ai.primitives.sampling.XofRandom(seed)[source]

Bases: object

Deterministic bit source: SHAKE-256 keyed by a secret seed.

Provides getrandbits for derandomised sampling (the same seed always yields the same bits). The seed must be secret and unpredictable. Call c (from 0) hashes len(seed) || seed || c; each call claims its c atomically, 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) raises RuntimeError.

Parameters:

seed (bytes) – Secret seed bytes (at least 32 bytes of entropy).

getrandbits(k)[source]
Parameters:

k (int)

Return type:

int

pyfe4ai.primitives.sampling.BIGINT_GUARD_BITS = 320

its uniforms and arithmetic have bits(sigma) + BIGINT_GUARD_BITS bits. 320 covers bits(sigma) + stat_param + ceil(log2 N) + 8 for every statistical parameter the key generators accept (at most MAX_STAT_PARAM = 256) and up to 2^56 samples per key: the dozen MPFR roundings of one sample stay below 2^-(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 the 2^-128 budgets 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 by check_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:

int

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. the sqrt(kappa) * N^{5/2} widths of the DCR schemes): a Box-Muller sample is computed with gmpy2.mpfr at bits(sigma) + 320 bits of precision (BIGINT_GUARD_BITS) from CSPRNG uniforms and rounded to the nearest integer. As with discrete_gaussian_matrix() this is a rounded continuous Gaussian, not an exact discrete-Gaussian sampler, and it is not constant-time. With sampler="facct" the samples come from discrete_gaussian_facct() (exact D_{Z,sigma}) with the same rng.

With the native backend (pyfe4ai.backend) the rounded samples of widths below 2^448 are computed by the extension when rng is an XofRandom or None: from the same getrandbits values, it returns the integers this function computes (see gaussian_bigint()); with rng=None its 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 with getrandbits (default: the OS CSPRNG). An XofRandom gives derandomised sampling.

  • sampler (str | None, default: None) – "rounded" or "facct"; None takes get_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:

list[mpz]

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 about 2^-185 of D_{Z,sigma}.

pyfe4ai.primitives.sampling.facct_base_width(sigma)[source]

(k, sigma / k): the double-sampling factor and the base width used by discrete_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 NormalDoubleConstant has the same structure). With k = ceil(sigma / sigma_0), sigma_0 = 1/sqrt(2 ln 2), and the base width sigma_b = sigma / k <= sigma_0, one trial:

  1. u <- getrandbits(P); x = number of CDT thresholds T_i <= u (the whole table is scanned), so x follows the half-Gaussian D_{Z>=0, sigma_b};

  2. y uniform in [0, k): getrandbits(bits(k - 1)) until below k (no bits when k = 1);

  3. v <- getrandbits(P); accept z = k x + y iff v < floor(2^P exp(-y (y + 2 k x) / (2 sigma^2))), so that z follows D_{Z>=0, sigma};

  4. s <- getrandbits(1); if z = 0 and s = 1 start over, else return (-1)^s z.

P is FACCT_PRECISION. Deviation from FACCT: its base is the fixed D_{Z>=0, sigma_0}, which only gives widths k sigma_0; here the base table is computed for sigma / 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 of getrandbits calls 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 same getrandbits in the same order and makes the same decisions when rng is an XofRandom or None (gaussian_facct(); with rng=None its bits come from the per-call DRBG).

Parameters:
  • sigma – Width (int, mpz, float, mpfr or mpq), > 0.

  • count (int) – Number of samples.

  • rng (default: None) – Bit source with getrandbits (default: os.urandom read in blocks). An XofRandom gives derandomised sampling.

Return type:

list[mpz]

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 + 1 this is the prime-order-q subgroup used by the DDH schemes. The digest is expanded with SHAKE-256 to bits(p) + 128 bits (so the reduction mod p is 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.

Parameters:
  • v (str) – Input string.

  • p (mpz) – Safe-prime modulus.

  • index (int, default: 0) – Output index, giving independent outputs for the same v.

  • domain (str, default: '') – Domain-separation tag.

Return type:

mpz

pyfe4ai.primitives.hashing.paillier_base(seed, n_mod)[source]

The base g = H(seed)^(2N) mod N^2 of 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 choose g (e.g. of order 2, or 1 + N), only another pseudo-random one.

Parameters:

seed (str)

Return type:

mpz

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) + 128 bits, so the reduction mod N^2 is statistically close to uniform; outputs not coprime to N (which would factor N) are re-hashed with a counter.

Parameters:
  • v (str) – Input string.

  • n_mod (mpz) – RSA modulus N.

  • domain (str, default: '') – Domain-separation tag.

Return type:

mpz

pyfe4ai.primitives.hashing.LABEL_HASHES = ('shake256-wide', 'shake256-rejection', 'shake128-rejection')

How hash_to_zq_vector() maps a string to Z_q (the label_hash setting of the MCFE schemes):

  • "shake256-wide": one SHAKE-256 stream cut into blocks of bits(q) + 128 bits, each reduced mod q (statistical distance below 2^-128 per 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, see zq_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-LWE a) and of MCFE public parameters and keys without a label_hash field.

pyfe4ai.primitives.hashing.DEFAULT_LABEL_HASH = 'shake128-rejection'

The label_hash the 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:

str

pyfe4ai.primitives.hashing.hash_to_zq_material(v, domain='', mode='shake256-wide')[source]

The XOF input of hash_to_zq_vector() (also hashed natively, see pyfe4ai.backend); the rejection modes use their own tag, so no two modes share an input.

Parameters:
  • v (str)

  • domain (str, default: '')

  • mode (str, default: 'shake256-wide')

Return type:

bytes

pyfe4ai.primitives.hashing.zq_rejection_parameters(q)[source]

(nb, limit) of the rejection modes of hash_to_zq_vector(): candidates are nb = ceil(bits(q) / 8) + 1 big-endian bytes of the stream, accepted below limit = floor(2^(8 nb) / q) q (more than 1 - 2^-8 of them) and reduced mod q. q | limit, so an accepted candidate is uniform mod q.

Return type:

tuple

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).

mode is one of LABEL_HASHES. "shake256-wide" (the default here, LEGACY_LABEL_HASH) cuts one SHAKE-256 output stream into length blocks of bits(q) + 128 bits, 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()).

Parameters:
  • v (str) – Input string.

  • q (mpz) – Modulus.

  • length (int) – Number of coordinates.

  • domain (str, default: '') – Domain-separation tag.

  • mode (str, default: 'shake256-wide') – Label-hash mode.

Return type:

list

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’s hash_to_zq().

Raises:

ValueError – For an unknown mode.

Parameters:
  • material (bytes)

  • q (mpz)

  • length (int)

  • mode (str, default: 'shake256-wide')

Return type:

list

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.

Parameters:

bits (int) – security parameters

Returns:

p, q

Return type:

tuple

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.

Parameters:
  • bits (int) – Security parameter (bit length).

  • r (int, default: 2) – Cofactor (default 2).

pyfe4ai.primitives.groups.integer_groups.is_dl_group(p, q, g, bits, cofactor=2)[source]

Whether p = cofactor q + 1 is a bits-bit prime, q is a prime of about bits bits (at most the cofactor’s bits fewer) and g generates the order-q subgroup of Z_p^*; at a size with an RFC 3526 prime, p must be that prime (another safe prime could carry a trapdoor).

Used to check discrete-log parameters loaded from the parameter cache.

Parameters:
  • bits (int)

  • cofactor (int, default: 2)

Return type:

bool

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 - 1 or 2 prime_bits bits.

Used to check DCR moduli loaded from the parameter cache (their factorisation is never stored, so it cannot be checked further).

Parameters:

prime_bits (int)

Return type:

bool

pyfe4ai.primitives.groups.integer_groups.group_generator_fe(bits, r=2)[source]

Generate a safe-prime group (p, q, r, g) for the DDH schemes.

p = r q + 1 is a bits-bit prime (for r = 2 the RFC 3526 prime at 2048 and 3072 bits) and g generates the order-q subgroup of Z_p^*.

Parameters:
  • bits (int) – Bit length of p (sec_param; not a security level).

  • r (int, default: 2) – Cofactor (default 2).

Return type:

tuple

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.

Parameters:
  • base (mpz) – Base for modular exponentiation.

  • exponent (mpz) – Exponent value.

  • modulus (mpz) – Modulus for the arithmetic operation.

Return type:

mpz

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]; e may 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^w digits into 2^w - 1 buckets and combines them (Brickell, Gordon, McCurley, Wilson, EUROCRYPT 1992, with the bucket method), about bits(e) / w + 2^(w + 1) products instead of bits(e) squarings. A negative exponent inverts the power of |e|. The results equal those of pow_signed().

Parameters:
  • base (mpz) – Base, invertible modulo modulus if an exponent is negative.

  • exponents – Integer exponents.

  • modulus (mpz) – Modulus.

Return type:

list

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:

list

pyfe4ai.primitives.groups.modular_utils.decode_paillier(acc, n_mod, bound)[source]

Return mu with acc = (1 + N)^mu mod N^2, centred, or None.

None when 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 + N turns ct0^{-k} into -k mod N).

Return type:

int | None

Discrete-Log Solver

Bounded discrete logarithm solvers for integer and pairing groups.

Provides two strategies:

  1. 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.

  2. 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].

Parameters:
  • 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.

Return type:

tuple[dict, int, mpz]

pyfe4ai.primitives.groups.dlog_solver.dlog_table_solve(value, g_str, p_str, bound, table, m, giant)[source]

Solve g^x ≡ value (mod p) with x ∈ [-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} for 0 <= j < m (keys as integers; tables keyed by digits(g^j) are still read).

  • m (int) – Baby-step count (table size).

  • giant (mpz) – Giant step g^-m mod p.

Return type:

int

pyfe4ai.primitives.groups.dlog_solver.load_or_build_dlog_table(filepath, g_str, p_str, bound)[source]

Return a dlog lookup table for g mod p covering [-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.

Parameters:
  • filepath (str) – Ignored (legacy cache location).

  • 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.

Return type:

tuple[dict, int, int, mpz]

pyfe4ai.primitives.groups.dlog_solver.bsgs_solve_int(value, g_str, p_str, bound)[source]

Solve g^x ≡ value (mod p) with x ∈ [-bound, bound] via BSGS.

Uses the standard two-pass approach (matching GoFE / CiFEr reference implementations): search [0, bound] with g, then search [0, bound] with g⁻¹ 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 a gmpy2.digits() string.

  • p_str (str) – Modulus as a gmpy2.digits() string.

  • bound (int) – Search range [-bound, bound].

Return type:

int

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 = target in a pairing group via on-the-fly BSGS.

Uses the standard approach (matching CiFEr’s cfe_baby_giant_FP12): build one baby-step table for e(g,h) and simultaneously check both target and target⁻¹ 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:
  • target – The target element in GT (output of a pairing).

  • g – Generator in G1.

  • h – Generator in G2.

  • pairing_group_param (str) – pairing group name (e.g. "BLS12-381").

  • bound (int) – Search range [-bound, bound].

Return type:

int

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() (ValueError above), 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.

Parameters:

what (str, default: 'scalar')

Return type:

int

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_.

Parameters:

purpose (str)

Return type:

bytes

pyfe4ai.primitives.groups.ec_group.hash_to_group(message, tag, index=0)[source]

hash_to_ristretto255(I2OSP(index, 1) || message, tag) (RFC 9380).

Parameters:
pyfe4ai.primitives.groups.ec_group.dlog(point, bound)[source]

The x with x G = point and |x| <= bound, or None.

Raises:

ValueError – If bound is negative or above MAX_DLOG_BOUND.

Parameters:

bound (int)

Return type:

int | None

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) with r = sqrt(u/v) (non-negative) if u/v is a square, else sqrt(i u / v).

Parameters:
Return type:

tuple[bool, int]

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 * BASE with a precomputed table of j 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:

bool

pyfe4ai.primitives.groups.ristretto255.encode(point)[source]

Canonical 32-byte encoding (RFC 9496 Sec. 4.3.2).

Return type:

bytes

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:

tuple

pyfe4ai.primitives.groups.ristretto255.from_uniform_bytes(data)[source]

RFC 9496 Sec. 4.3.4: the group element of 64 uniform bytes.

Return type:

tuple

pyfe4ai.primitives.groups.ristretto255.expand_message_xmd(msg, dst, length)[source]

RFC 9380 Sec. 5.3.1 with SHA-512.

Parameters:
Return type:

bytes

pyfe4ai.primitives.groups.ristretto255.hash_to_group(msg, dst)[source]

hash_to_ristretto255 of RFC 9380 (ristretto255_XMD:SHA-512_R255MAP_RO_).

Parameters:
Return type:

tuple

pyfe4ai.primitives.groups.ristretto255.random_scalar()[source]

A uniform scalar in [0, L) from the OS CSPRNG.

Return type:

int

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:

bool

class pyfe4ai.primitives.pairing.pairing_backend.PairingGroup(name='BLS12-381', *args, **kwargs)[source]

Bases: object

A 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:

list

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:

list

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:

None

pyfe4ai.primitives.pairing.pairing_backend.pairing_available(name='BLS12-381')[source]

Whether PairingGroup() can build the group name.

Return type:

bool

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 = G2 and DDH is easy. A group is symmetric iff it pairs two G1 elements.

Parameters:

group – A PairingGroup or a group name such as "BLS12-381".

Raises:

FEValidationError – If the group is symmetric.

Return type:

None

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.transpose_mod(matrix)[source]
Parameters:

matrix (list[list[mpz]])

Return type:

list[list[mpz]]

pyfe4ai.primitives.pairing.pairing_utils.mat_vec_mod(matrix, vector, modulus)[source]

Multiply a matrix by a vector modulo a given modulus.

Parameters:
  • matrix (list[list[mpz]]) – Input matrix.

  • vector (list[mpz]) – Input vector.

  • modulus (mpz) – Modulus for the arithmetic operation.

Return type:

list[mpz]

pyfe4ai.primitives.pairing.pairing_utils.vector_add_mod(lhs, rhs, modulus)[source]

Add two vectors element-wise modulo a given modulus.

Parameters:
  • lhs (list[mpz]) – Left-hand side vector.

  • rhs (list[mpz]) – Right-hand side vector.

  • modulus (mpz) – Modulus for the arithmetic operation.

Return type:

list[mpz]

pyfe4ai.primitives.pairing.pairing_utils.vector_scalar_mod(vector, scalar, modulus)[source]

Multiply a vector by a scalar modulo a given modulus.

Parameters:
  • vector (list[mpz]) – Input vector.

  • scalar (mpz) – Scalar multiplier.

  • modulus (mpz) – Modulus for the arithmetic operation.

Return type:

list[mpz]

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:
pyfe4ai.primitives.pairing.pairing_utils.matrix_inverse_mod(matrix, modulus)[source]

Compute the modular inverse of a matrix via Gaussian elimination.

Parameters:
  • matrix (list[list[mpz]]) – Input matrix.

  • modulus (mpz) – Modulus for the arithmetic operation.

Return type:

tuple[list[list[mpz]], mpz]

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 (False for a base used once; see pyfe4ai.primitives.pairing.bls12_381.gt_dlog()).

Return type:

int

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:

list[list[str]]

pyfe4ai.primitives.pairing.pairing_utils.deserialize_matrix_g(group, matrix)[source]

Deserialize a matrix of strings back to group elements.

Parameters:

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() (ValueError above).

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: object

BLS12-381 with the methods of charm’s PairingGroup that the schemes use.

Parameters:

name (str, default: 'BLS12-381')

init(kind, value=None)[source]

ZR(value), or the identity of G1 / G2 / GT (charm’s init(GT, 1)).

generator(kind)[source]

The fixed generator of G1 or G2, or e(g1, g2) for GT.

deserialize_many(items)[source]

[deserialize(x) for x in items] in one call; equal encodings are decoded once.

Return type:

list

hash(msg, kind=1)[source]

H(msg) in G1 or G2; msg is bytes or a str (hashed as UTF-8).

pyfe4ai.primitives.pairing.bls12_381.pair(a, b)[source]

e(a, b) for a in G1 and b in G2.

pyfe4ai.primitives.pairing.bls12_381.multi_pair(lhs, rhs)[source]

prod_i e(lhs[i], rhs[i]) with one final exponentiation.

Parameters:
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.

Parameters:
pyfe4ai.primitives.pairing.bls12_381.pow_many(base, scalars)[source]

[base ** k for k in scalars] with one precomputation for base.

Parameters:

scalars (list)

Return type:

list

pyfe4ai.primitives.pairing.bls12_381.gt_dlog(base, target, bound, cache=True)[source]

The x with base^x = target and |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; pass cache=False for a base used once (a table of about sqrt(2 bound + 1) steps, not kept).

Raises:

ValueError – If bound is negative or above MAX_DLOG_BOUND.

Parameters:
  • bound (int)

  • cache (bool, default: True)

pyfe4ai.primitives.pairing.bls12_381.is_element(obj)[source]

Whether obj is an element of this backend (not of charm).

Return type:

bool