MCFE — Multi-Client Functional Encryption

Multi-client functional encryption schemes: the key generator and crypto class of every scheme of the family (the registry: pyfe4ai.core.registry).

The key generator and crypto class of every scheme below are importable from pyfe4ai.schemes.mcfe under their own names (one name per class, no aliases; the naming rule is in pyfe4ai.core.registry).

DDH

Chotard, Jérémy, Edouard Dufour Sans, Romain Gay, Duong Hieu Phan, and David Pointcheval. “Decentralized multi-client functional encryption for inner product.” In Advances in Cryptology ASIACRYPT 2018, Brisbane, QLD, Australia, December 2-6, pp. 703-732. Springer International Publishing, 2018. | URL: https://eprint.iacr.org/2017/989

  • type: secret-key multi-client encryption (MCFE, DDH, random oracle)

  • setting: Integer based (order-q subgroup of Z_p^*, p = 2q + 1)

Construction (one coordinate j of client i’s vector is one CDGPP18 slot):

  • Setup: s_{i,j} <- Z_q^2 for every client i and coordinate j.

  • Encrypt: [u_l] = H(l) in G^2; c_{i,j} = g^{x_{i,j}} * [u_l]^{s_{i,j}}.

  • KeyGen: d = sum_{i,j} y_{i,j} * s_{i,j} in Z_q^2 (one aggregated key).

  • Decrypt: g^{<x,y>} = prod c_{i,j}^{y_{i,j}} / [u_l]^d, then a bounded dlog.

Each client must encrypt at most once per label: two ciphertexts of the same client under the same label reveal the difference of the plaintexts.

class pyfe4ai.schemes.mcfe.ddh.MCFEDDHKeyGenerator(config, **kwargs)[source]

Bases: DDHKeyGeneratorBase

Key generator for DDH-based multi-client inner-product FE.

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the MCFE key generator.

Parameters:

config (dict) – Scheme configuration. Recognised keys: sec_param, eta (int or per-client dict), n (number of clients), s, lst_nid (client identifiers).

Return type:

None

setup()[source]

Generate one independent secret s_{i,j} in Z_q^2 per client slot.

Return type:

None

get_public_parameters()[source]

Return serialisable public parameters (g, p, n, eta, sec_param).

Return type:

dict

Returns:

Dict of public parameters.

get_private_keys(nid)[source]

Return the encryption key of client nid.

Parameters:

nid (str) – Client identifier, must be in lst_nid.

Return type:

dict | None

Returns:

Dict with s (one Z_q^2 pair per slot of nid), or None if nid is invalid.

get_decryption_keys(sid, **kwargs)[source]

Derive the functional decryption key d = sum y_{i,j} s_{i,j}.

Parameters:
  • sid (str) – Session identifier.

  • **kwargs – Must include credentials with fusion_weight (dict mapping client IDs to weight lists).

Return type:

dict | None

Returns:

Dict with d (a pair of integers mod q). The key is aggregated over all clients, so it gives no per-client decryption capability.

Raises:
  • FEKeyError – If credentials are missing.

  • FEValidationError – If a client id is unknown or a weight list is longer than that client’s eta.

class pyfe4ai.schemes.mcfe.ddh.MCFEDDH(config)[source]

Bases: OfflineMCFEMixin, IPFEAbsCrypto

Crypto operations for DDH-based multi-client inner-product FE.

Parameters:

config (dict)

__init__(config)[source]

Initialise the MCFE crypto system.

Parameters:

config (dict) – Must contain keys with pp (public parameters) and either sk (encryption keys) or dk (decryption key). dlog_bound (default 10^(precision + 2)) bounds |<x, y>| for the final discrete log.

Return type:

None

encrypt(lst_pt, label)[source]

Encrypt a plaintext vector for one client under a label.

Parameters:
  • lst_pt (list) – Integer plaintext vector of length ≤ eta.

  • label (str) – Encryption label binding the ciphertext to a session. A client must not encrypt twice under the same label.

Return type:

dict

Returns:

Dict with c (one group element per plaintext slot).

Raises:
decrypt(dct_ct, dk, fusion_weight, label, term=None)[source]

Decrypt aggregated ciphertexts to recover the multi-client inner product.

Parameters:
  • dct_ct (dict) – Mapping of client IDs to their ciphertext dicts.

  • dk (dict) – Decryption key dict with d (pair of integers mod q).

  • fusion_weight (dict) – Mapping of client IDs to weight lists; must be the weights the key was derived for.

  • label (str) – The encryption label used during encrypt().

  • term (default: None) – prod_k H_k(l)^{d_k} mod p if already computed (prepare_key()).

Returns:

The inner product as an integer, or None if out of bound.

Raises:
  • FEKeyError – If decryption key is missing.

  • FESchemeError – If ciphertexts and fusion weights cover different clients.

encrypt_lst_ndarray(lst_ndarray, **kwargs)[source]

Encrypt a list of ndarrays element-wise with a label.

Each element is encrypted under its own label derived from label, the layer and the element index (see pyfe4ai.schemes.mcfe.labels).

Parameters:
  • lst_ndarray (list) – List of numpy arrays to encrypt.

  • **kwargs – Must include label (a string).

Return type:

list | None

Returns:

List of object ndarrays containing per-element ciphertexts.

decrypt_lst_ndarray_ct(dict_ndarray_ct, dk, fusion_weight, label)[source]

Decrypt ndarray ciphertexts from multiple clients.

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to ciphertext ndarray lists.

  • dk (dict) – Decryption key dict.

  • fusion_weight (dict) – Mapping of client IDs to weight lists.

  • label (str) – The encryption label.

Return type:

list | None

Returns:

List of float ndarrays with decrypted values.

compute_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Aggregate ndarray ciphertexts (delegates to decrypt_lst_ndarray_ct()).

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to ciphertext ndarray lists.

  • **kwargs – Must include dk, fusion_weight, and label.

Return type:

list | None

Returns:

Decrypted ndarray list.

DDH over ristretto255

Multi-client inner-product FE (DDH) over ristretto255 | Chotard, Dufour Sans, Gay, Phan, Pointcheval: | “Decentralized multi-client functional encryption for inner product”, | ASIACRYPT 2018, https://eprint.iacr.org/2017/989 – the MCFE of Sec. 4.

  • type: secret-key multi-client encryption (MCFE, DDH, random oracle)

  • setting: the prime-order group ristretto255 (RFC 9496), written additively

As pyfe4ai.schemes.mcfe.ddh in ristretto255 (one coordinate j of client i is one CDGPP18 slot):

  • Setup: s_{i,j} <- Z_L^2.

  • Encrypt: [u_l] = H(l) in G^2; c_{i,j} = x_{i,j} G + <s_{i,j}, [u_l]>.

  • KeyGen: d = sum_{i,j} y_{i,j} s_{i,j} in Z_L^2 (one aggregated key).

  • Decrypt: sum y_{i,j} c_{i,j} - <d, [u_l]> = <x, y> G, then a bounded dlog.

H(l)_k = hash_to_ristretto255(k || encode_label(l)) (RFC 9380, DST PYFE4AI-MCFE-DDH-H(label)-V01-CS02-with-ristretto255_XMD:SHA-512_R255MAP_RO_). Each client must encrypt at most once per label; the ndarray helpers derive one label per element.

pyfe4ai.schemes.mcfe.ddh_ristretto255.hash_label(label)[source]

[u_l] = H(label) in G^2 (random oracle into ristretto255).

Return type:

list

class pyfe4ai.schemes.mcfe.ddh_ristretto255.MCFEDDHRistretto255KeyGenerator(config, **kwargs)[source]

Bases: ECDDHKeyGeneratorBase

Key generator for DDH-based multi-client inner-product FE over ristretto255.

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Check the configuration.

Parameters:

config (dict) – n (number of clients), lst_nid, eta (int or per-client dict), group (optional, "ristretto255").

Return type:

None

setup()[source]

One secret s_{i,j} in Z_L^2 per client slot.

Return type:

None

get_public_parameters()[source]

Return serialisable public parameters.

Return type:

dict

Returns:

A dict of public parameters, or None on error.

get_private_keys(nid)[source]

Client nid’s key {s} (one Z_L^2 pair per slot), None if unknown.

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

{d: sum y_{i,j} s_{i,j}} (a pair of scalars), aggregated over all clients.

Parameters:

sid (str)

Return type:

dict | None

class pyfe4ai.schemes.mcfe.ddh_ristretto255.MCFEDDHRistretto255(config, **kwargs)[source]

Bases: ECDDHCryptoBase

Crypto operations for DDH-based multi-client inner-product FE over ristretto255.

Parameters:

config (dict)

encrypt(lst_pt, label)[source]

{c: [x_j G + <s_j, H(label)>]}; a client encrypts at most once per label.

Parameters:
Return type:

dict

decrypt(dct_ct, dk, fusion_weight, label)[source]

sum_i <x_i, y_i> under label, or None if beyond dlog_bound.

Raises:
Parameters:
encrypt_lst_ndarray(lst_ndarray, **kwargs)[source]

Encrypt ndarrays element-wise, every element under its own label.

Parameters:

lst_ndarray (list)

Return type:

list | None

decrypt_lst_ndarray_ct(dict_ndarray_ct, dk, fusion_weight, label)[source]

Decrypt aggregated ndarray ciphertexts.

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to their ciphertext lists.

  • dk (dict)

  • fusion_weight (dict)

  • label (str)

Return type:

list | None

Returns:

One float array of decrypted results per layer.

Raises:

FESchemeError – If an element does not decrypt (see decoded_layer()).

compute_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Aggregate ciphertexts from multiple clients.

Parameters:

dict_ndarray_ct (dict) – Mapping of client IDs to their ciphertext lists.

Return type:

list | None

Returns:

Aggregated ciphertext list, or None on error.

DDH Decentralized

Decentralised multi-client inner-product FE over DDH.

Abdalla, Bourse, Marival, Pointcheval, Soleimanian, Waldner:
“Multi-Client Inner-Product Functional Encryption in the Random-Oracle Model”
SCN 2020, https://eprint.iacr.org/2020/788 – Appendix D.1, Fig. 14
(DMCFE from MCFE), instantiated with
MCFE1 = CDGPP18 (ASIACRYPT 2018, pyfe4ai.schemes.mcfe.ddh) and
MCFE2 = the DCR MCFE of ABMPSW20 Fig. 3 (pyfe4ai.schemes.mcfe.decentralize).
  • type: decentralised multi-client encryption (DMCFE, DDH + DCR, random oracle)

  • setting: Integer based (order-q subgroup of Z_p^*; Z_{N2^2}^* for key shares)

Encryption and decryption are those of CDGPP18 (c = g^x [H(l)]^s, g^{<x,y>} = prod c^y / [H(l)]^d). There is no authority for functional keys: for weights y every client i publishes an MCFE2 encryption of its contribution c_i = (sum_j y_{i,j} s_{i,j} mod q) + q rho_i (a pair) under the label H(y); the zero-sum MCFE2 keys make the product of all clients’ shares decrypt to sum_i c_i, whose residue mod q is the CDGPP18 functional key d. Every client must publish a share (with zero weights if absent from y).

The carry mask rho_i is our own adaptation. ABMPSW20 states Fig. 14 for MCFEs that need no discrete logarithm when decrypting (DCR, LWE): with CDGPP18 the secrets are uniform mod q, so sum_i y_i s_i is not small. Reducing each contribution mod q (as versions before this one did) makes the integer sum d + q carry with carry in 0..n-1, which anyone holding the published shares reads off: a comparison oracle on the clients’ reduced contributions, from which a series of keys reveals their secrets. Here rho_i is uniform below 2^(stat_param + bits(n)) and derived from a per-client key and H(y) (repeating a derivation repeats the share, so masks cannot be averaged out), and carry + sum_i rho_i is within statistical distance 2^-stat_param of a carry-independent distribution. The argument is ours, hence the scheme’s experimental status; Theorem 38 covers the MCFE2 layer only.

class pyfe4ai.schemes.mcfe.ddh_decentralized.DecentralizedMCFEDDHKeyGenerator(config, **kwargs)[source]

Bases: MCFEDDHKeyGenerator

Sets up CDGPP18 client keys and zero-sum MCFE2 keys (ABMPSW20 Fig. 14).

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the key generator.

Parameters:

config (dict) – As for MCFEDDHKeyGenerator (sec_param, eta, n, lst_nid), plus stat_param for MCFE2.

Return type:

None

setup()[source]

Sample the CDGPP18 client secrets and the zero-sum MCFE2 keys.

Return type:

None

get_public_parameters()[source]

CDGPP18 public parameters plus n2_mod, lst_nid and the contribution bound.

Return type:

dict

get_private_keys(nid)[source]

Return client nid’s keys: CDGPP18 s, MCFE2 s2 and the carry-mask key.

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

Not available: functional keys are derived by the clients.

Parameters:

sid (str)

Return type:

dict | None

class pyfe4ai.schemes.mcfe.ddh_decentralized.DecentralizedMCFEDDH(config)[source]

Bases: MCFEDDH

CDGPP18 encryption / decryption with decentralised key derivation.

Parameters:

config (dict)

derive_function_decryption_key_share(fusion_weight)[source]

Publish this client’s encrypted key contribution for weights y.

Parameters:

fusion_weight (dict) – Mapping of client IDs to integer weight lists (the whole y; clients that are absent contribute zero).

Return type:

dict

Returns:

Dict with c (MCFE2 ciphertexts), nid and weights_id.

combine_function_decryption_key_share(dct_dk_shares)[source]

Combine every client’s share into the CDGPP18 key {d}.

Raises:

FESchemeError – If a client’s share is missing or the shares were derived for different weights.

Parameters:

dct_dk_shares (dict)

Return type:

dict

DDH Threshold

Threshold multi-client inner-product FE over DDH: CDGPP18 MCFE with a Shamir-shared functional key.

MCFE layer: Chotard, Dufour Sans, Gay, Phan, Pointcheval,
“Decentralized multi-client functional encryption for inner product”,
ASIACRYPT 2018, https://eprint.iacr.org/2017/989 (MCFE from DDH in the ROM;
same construction as pyfe4ai.schemes.mcfe.ddh).
Threshold layer in the setting of
R. Xu et al., “TAPFed: Threshold Secure Aggregation for Privacy-Preserving
Federated Learning”, IEEE TDSC, doi: 10.1109/TDSC.2024.3350206: the
aggregated functional key is Shamir-shared among s decryption servers
and any t of them decrypt by Lagrange interpolation in the exponent.
  • type: secret-key multi-client encryption, threshold functional decryption

  • setting: Integer based (order-q subgroup of Z_p^*, p = 2q + 1), random oracle

Construction (one coordinate j of client i’s vector is one CDGPP18 slot):

  • Setup: s_{i,j} <- Z_q^2 for every client slot.

  • Encrypt: [u_l] = H(l) in G^2; c_{i,j} = g^{x_{i,j}} * [u_l]^{s_{i,j}}.

  • KeyGen: d = sum y_{i,j} s_{i,j} in Z_q^2; each component of d is Shamir-shared with threshold t over Z_q at points 1..s; server k receives d^(k) in Z_q^2.

  • ShareDec: server k of an enrolled set E (|E| >= t) publishes P_k = [u_l]^{lambda_k * d^(k)} with lambda_k its Lagrange coefficient at 0 for E (mod q), and C_y = prod c_{i,j}^{y_{i,j}}.

  • Combine: [u_l]^d = prod_{k in E} P_k; g^{<x,y>} = C_y / [u_l]^d, then a bounded dlog.

The functional key is a single aggregated value, so neither a server nor the combiner can decrypt any individual client; fewer than t shares of d are independent of d. Each client must encrypt at most once per label: two ciphertexts of the same client slot under the same label reveal the difference of the plaintexts. The ciphertext carries its (public) label so that the servers can evaluate H(l).

Deviation from earlier versions (<= 0.2.0): the label used to enter as a public MD5 scalar and the key carried one share per client, which let the combiner strip each client’s mask; see CHANGELOG 0.3.0.

class pyfe4ai.schemes.mcfe.ddh_threshold.ThresholdMCFEDDHKeyGenerator(config, **kwargs)[source]

Bases: DDHKeyGeneratorBase

Key generator for threshold DDH-based multi-client inner-product FE.

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the threshold MCFE key generator.

Parameters:

config (dict) – Scheme configuration. Recognised keys: sec_param, eta (slots per client), n (number of clients), s (number of decryption servers), t (threshold), lst_nid and lst_sid.

Return type:

None

setup()[source]

Generate one independent secret s_{i,j} in Z_q^2 per client slot.

Return type:

None

get_public_parameters()[source]

Return serialisable public parameters (p, g, eta, n, s, t, sec_param).

Return type:

dict

get_private_keys(nid)[source]

Return the encryption key of client nid.

Parameters:

nid (str) – Client identifier, must be in lst_nid.

Return type:

dict | None

Returns:

Dict with s (one Z_q^2 pair per slot), or None if nid is invalid.

get_decryption_keys(sid, **kwargs)[source]

Return server sid’s Shamir share of the functional key for y.

All servers asking for the same fusion weights receive shares of one and the same sharing. The key does not depend on the label; a label entry in the credentials is accepted and ignored.

Parameters:
  • sid (str) – Decryption-server identifier, must be in lst_sid.

  • **kwargs – Must include credentials with fusion_weight (dict mapping client IDs to integer weight lists).

Return type:

dict | None

Returns:

Dict with d (this server’s share, a pair mod q), sid, lst_sid and weights_id (digest of the weights), or None if sid is unknown.

Raises:
class pyfe4ai.schemes.mcfe.ddh_threshold.ThresholdMCFEDDH(config)[source]

Bases: IPFEAbsCrypto

Crypto operations for threshold DDH-based multi-client inner-product FE.

Parameters:

config (dict)

__init__(config)[source]

Initialise a client (keys.sk), server (keys.dk) or combiner.

Parameters:

config (dict) – Must contain keys with pp and optionally sk or dk; id names the client / server; precision is the number of decimal digits kept when encrypting floats; dlog_bound (default 10^(precision + 2)) bounds |<x, y>| (after scaling) for the final discrete log.

Return type:

None

encrypt(pt, label)[source]

Encrypt one client’s vector under a label.

Values are scaled by 10^precision and rounded to integers.

Parameters:
  • pt (list) – Plaintext vector of length at most eta.

  • label (str) – Encryption label; a client must not encrypt twice under the same label.

Return type:

dict

Returns:

Dict with c (one group element per slot) and label (the encoded label, see pyfe4ai.schemes.mcfe.labels.encode_label()).

Raises:
share_decrypt(dict_ct, credentials, dk, lst_sid_enrolled, label)[source]

Compute this server’s partial decryption for an enrolled set.

Parameters:
  • dict_ct (dict) – Mapping of client IDs to ciphertexts (all under one label).

  • credentials (dict) – Mapping of client IDs to the fusion weights the key share was derived for.

  • dk (dict) – This server’s key share from get_decryption_keys.

  • lst_sid_enrolled (list) – The servers whose partial decryptions will be combined (at least t, including this one).

  • label – The label the caller wants decrypted; ciphertexts under any other label (e.g. those of an earlier round) are refused.

Return type:

dict

Returns:

Dict with c_y (prod c^y), u_d ([u_l]^{lambda d^(k)}), label, sid and lst_sid_enrolled.

Raises:
  • FEKeyError – If the key share is missing or malformed.

  • FESchemeError – If the server or enrolled set is invalid, or the ciphertexts do not match the fusion weights / a single label.

combine_decrypt(partials, label=None)[source]

Combine partial decryptions from at least t servers.

Parameters:
  • partials – {sid: share_decrypt(...)} for one enrolled set, or a list of such mappings for several enrolled sets, whose results must then agree (see combine_consistently(): servers are assumed honest-but-curious, and only this redundancy detects one that returns a wrong partial).

  • label (default: None) – Optional label the partials must be for.

Raises:

FESchemeError – If a group is incomplete or invalid, is for another label or its partial decryptions disagree on the ciphertexts they decrypted, or the groups disagree.

Return type:

float | None

encrypt_lst_ndarray(lst_ndarray, **kwargs)[source]

Encrypt a list of ndarrays element-wise (eta = 1 slots).

Every element is encrypted under its own label derived from label, the layer and the element index, because a client must not encrypt twice under one label.

Parameters:
  • lst_ndarray (list) – List of numpy arrays.

  • **kwargs – Must include label.

Return type:

list

compute_lst_ndarray_ct(dict_ndarray_ct, credentials, dk, lst_sid_enrolled, label)[source]

Partially decrypt every element of clients’ ndarray ciphertexts.

Element (l, i) must be encrypted under its element label derived from label, so ciphertexts of another round or position are refused.

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to ciphertext ndarray lists.

  • credentials (dict) – Mapping of client IDs to fusion weights.

  • dk (dict) – This server’s key share.

  • lst_sid_enrolled (list) – The servers whose shares will be combined.

  • label (str) – The label the ndarrays were encrypted under.

Return type:

list

decrypt_lst_ndarray_ct(dict_ndarray_ct, label)[source]

Combine element-wise partial decryptions from at least t servers.

Parameters:
  • dict_ndarray_ct – Mapping of server ids to the outputs of compute_lst_ndarray_ct() (or a list of such mappings for several enrolled sets, see combine_decrypt()).

  • label (str) – The label of the ndarrays; every partial must be for the element label of its position.

Raises:

FESchemeError – If the servers’ outputs differ in structure, an element does not decrypt (out of range), or the partial decryptions of an element disagree on the ciphertexts (the error and message of combine_decrypt).

Return type:

list

static L(sid, sid_v, lst_sid_enrolled, modulus)[source]

Compute the Lagrange coefficient of sid at 0 modulo the group order.

Parameters:
  • sid – Decryption-server identifier.

  • sid_v (dict) – Mapping of server ids to their (1-based) share points.

  • lst_sid_enrolled (list) – List of enrolled server identifiers.

  • modulus – Group order q.

Return type:

mpz

Damgård DDH

MDDH-based multi-client inner-product FE (Damgard-style DDH family).

Abdalla, Bourse, Marival, Pointcheval, Soleimanian, Waldner:
“Multi-Client Inner-Product Functional Encryption in the Random-Oracle Model”
SCN 2020, https://eprint.iacr.org/2020/788 – Fig. 2 (MCFE based on the
D_k-MDDH assumption), proof in Appendix A (one-IND secure in the ROM).
  • type: secret-key multi-client encryption (MCFE, MDDH, random oracle)

  • setting: Integer based (order-q subgroup of Z_p^*, p = 2q + 1)

Construction (one coordinate j of client i’s vector is one slot; [a] denotes g^a):

  • Setup: H: labels -> G^{k+1}.

  • KeyGen: S_{i,j} <- Z_q^{k+1} per slot.

  • Encrypt: [u_l] = H(l); c_{i,j} = [S_{i,j} . u_l + x_{i,j}].

  • KeyDer: sk_y = sum y_{i,j} S_{i,j} in Z_q^{k+1} (one aggregated key).

  • Decrypt: [<x, y>] = prod c_{i,j}^{y_{i,j}} / [u_l . sk_y], then a bounded discrete log.

k is the config key mddh_k (default 1). For k = 1 (DDH) this is exactly the CDGPP18 MCFE of pyfe4ai.schemes.mcfe.ddh, and it is also what ABMPSW20’s approach (replace the IPFE randomness (g^r, h^r) by H(l)) yields from the Damgard-style DDH IPFE of Agrawal, Libert, Stehle (CRYPTO 2016), which is why the registry keeps the name damgard for it. k >= 2 gives the k-Lin instantiation (a weaker assumption, k + 1 group elements per label hash and key).

Each client slot must encrypt at most once per label (one-IND security); the ndarray helpers derive a distinct label per element. H hashes into the quadratic-residue subgroup with SHAKE-256.

class pyfe4ai.schemes.mcfe.damgard_ddh.MCFEDamgardKeyGenerator(config, **kwargs)[source]

Bases: IPFEAbsKeyGenerator, ParameterCacheMixin

Key generator for MDDH-based multi-client inner-product FE (ABMPSW20 Fig. 2).

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the key generator.

Parameters:

config (dict) – Scheme configuration. Recognised keys: n (number of clients), lst_nid, eta (slots per client), modulus_length (bits of p, default sec_param), bound (bound on |x| and |y|) and mddh_k.

Return type:

None

setup()[source]

Sample S_{i,j} <- Z_q^{k+1} for every client slot.

Return type:

None

get_public_parameters()[source]

Return serialisable public parameters.

Return type:

dict

get_private_keys(nid, **kwargs)[source]

Return the encryption key {s} of client nid (None if unknown).

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

Derive the functional key sk_y = sum y_{i,j} S_{i,j} in Z_q^{k+1}.

Parameters:
  • sid (str) – Session identifier (unused).

  • **kwargs – Must include credentials with fusion_weight (dict mapping client IDs to integer weight lists).

Return type:

dict | None

Returns:

Dict with d, one vector aggregated over all clients.

Raises:
  • FEKeyError – If credentials are missing.

  • FEValidationError – If a client id is unknown or a weight is malformed or exceeds bound.

class pyfe4ai.schemes.mcfe.damgard_ddh.MCFEDamgard(config, **kwargs)[source]

Bases: OfflineMCFEMixin, IPFEAbsCrypto

Crypto operations for MDDH-based multi-client inner-product FE (ABMPSW20 Fig. 2).

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise with keys.pp and, for clients, keys.sk.

Parameters:

config (dict)

Return type:

None

encrypt(lst_pt, label)[source]

Encrypt one client’s integer vector under a label.

Parameters:
  • lst_pt (list) – Integer plaintext vector of length at most eta.

  • label (str) – Encryption label; a client must not encrypt twice under the same label.

Return type:

dict

Returns:

Dict with c (one group element per slot).

Raises:
  • FEKeyError – If public parameters or encryption keys are missing.

  • FEValidationError – If the plaintext is malformed or out of bound.

decrypt(dct_ct, dk, fusion_weight, label, term=None)[source]

Recover sum_i <x_i, y_i> from all clients’ ciphertexts under one label.

Parameters:
  • dct_ct (dict) – Mapping of client IDs to ciphertext dicts.

  • dk (dict) – Functional key {d} derived for fusion_weight.

  • fusion_weight (dict) – Mapping of client IDs to weight lists.

  • label (str) – The encryption label.

  • term (default: None) – prod_m H_m(l)^{d_m} mod p if already computed (prepare_key()).

Returns:

The inner product, or None if it is out of the dlog bound (e.g. wrong label or key).

Raises:
encrypt_lst_ndarray(lst_ndarray, **kwargs)[source]

Encrypt ndarrays element-wise (eta = 1), one derived label per element.

Parameters:
  • lst_ndarray (list) – List of numpy arrays.

  • **kwargs – Must include label.

Return type:

list | None

compute_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Aggregate-decrypt ndarray ciphertexts (delegates to decrypt_lst_ndarray_ct()).

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to ciphertext ndarray lists.

  • **kwargs – Must include dk, fusion_weight and label.

Return type:

list | None

decrypt_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Decrypt ndarray ciphertexts element-wise.

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to ciphertext ndarray lists.

  • **kwargs – Must include dk, fusion_weight and label.

Return type:

list | None

LWE

LWE-based multi-client inner-product FE.

Abdalla, Bourse, Marival, Pointcheval, Soleimanian, Waldner:
“Multi-Client Inner-Product Functional Encryption in the Random-Oracle Model”
SCN 2020, https://eprint.iacr.org/2020/788 – Fig. 4 (MCFE based on LWE),
correctness and security in Appendix C.2, parameters in Appendix C.3
(one-IND secure in the ROM under LWE). The construction is the MCFE
counterpart of the LWE IPFE of Agrawal, Libert, Stehle (CRYPTO 2016).
  • type: secret-key multi-client encryption (MCFE, LWE, random oracle)

  • setting: Integer based (Z_q with a rounding map to Z_q0)

Construction (one coordinate j of client i’s vector is one slot; n is the total number of slots, Delta = floor(q / K)):

  • Setup: H: labels -> Z_q^{n0 + m0} full-domain hash.

  • KeyGen: Z_{i,j} = (s_{i,j}, t_{i,j}) with s <- U(Z_q^{n0}) and t <- D_{Z^{m0}, sigma_t}, sampled exactly by default (gaussian_sampler = "facct", recorded in the public parameters), as the statistical step of the proof (Lemma 34) assumes; "rounded" (a rounded continuous Gaussian, 2^-25..2^-15 from it per key in the October 2026 crypto review) stays selectable.

  • Encrypt: c_{i,j} = round_q0( Z_{i,j} . H(l) + Delta * x_{i,j} ).

  • KeyDer: sk_y = sum y_{i,j} Z_{i,j} (one aggregated vector, kept mod q).

  • Decrypt: mu' = sum y_{i,j} c_{i,j} - round_q0( sk_y . H(l) ) mod q0; output the mu closest to mu' q / (q0 Delta).

Deviation from the paper: messages and weights are signed (|x| <= X, |y| <= Y) instead of {0..P-1} / {0..V-1}; the inner product is encoded with K = 2 n X Y + 1 and decoded centred. Parameters are derived by pyfe4ai.primitives.lattice.lwe_utils.abmpsw20_lwe_parameters() from the constraints of Appendix C.3; this module uses the selective-security choice of m0 and pyfe4ai.schemes.mcfe.fullysec_lwe the adaptive one. The LWE dimension lwe_n defaults to a toy value; lwe_n = "auto" (the standard security profile) selects the smallest dimension whose q meets pyfe4ai.primitives.lattice.lwe_utils.DENSE_LWE_128_LOG_Q, a lattice estimator bound for LWE(n0, q, ceil(2 sqrt(n0))) – the assumption of the proof (Appendix C.2, game G2: LWE in dimension n0 modulo the scheme’s q) – with a uniform secret.

Each client slot must encrypt at most once per label (one-IND security): two ciphertexts of one slot under one label differ by about (q0 / q) Delta (x - x'). The ndarray helpers therefore derive a distinct label per element. H is SHAKE-256 (the paper’s implementation uses counter-mode SHA-256), mapped to Z_q as the key generator’s label_hash says (a public parameter; LABEL_HASHES): by default rejection sampling mod q from SHAKE128 (exactly uniform, fewer XOF bytes). Versions <= 0.3.0 reduced bits(q) + 128-bit SHAKE-256 blocks ("shake256-wide", still selectable), so their ciphertexts differ. The mode is bound to the public parameters, the clients’ encryption keys, every single ciphertext and every ndarray layer (label_hash: the code of LABEL_HASH_CODES, absent for "shake256-wide"; in an ndarray layer only its first element carries it, for the whole layer); a missing field means "shake256-wide" (the data of versions <= 0.3.0 stay readable), and encryption with a key of another mode or decryption of a ciphertext of another mode raises instead of returning a wrong sum. Functional keys sum y Z do not depend on the hash.

pyfe4ai.schemes.mcfe.lwe.hash_label(label, q, length, mode)[source]

Return H(label) in Z_q^length (random oracle; mode: the label_hash public parameter).

Parameters:
Return type:

list

pyfe4ai.schemes.mcfe.lwe.dot_mod(left, right, q)[source]

Return <left, right> mod q for integer vectors.

Parameters:
Return type:

mpz

class pyfe4ai.schemes.mcfe.lwe.MCFELWEKeyGenerator(config, **kwargs)[source]

Bases: IPFEAbsKeyGenerator

Key generator for LWE-based multi-client inner-product FE (ABMPSW20 Fig. 4).

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the key generator and derive the parameters.

Parameters:

config (dict) – Scheme configuration. Recognised keys: n (number of clients), lst_nid, eta (slots per client), lwe_n (LWE dimension n0; default 32, "auto" for the smallest one meeting DENSE_LWE_128_LOG_Q, the standard profile’s choice), bound_x, bound_y, stat_param (statistical parameter kappa), sec_param (recorded only), gaussian_sampler (that of the keys t: "facct", the default, or "rounded"; see scheme_sampler()) and label_hash (one of LABEL_HASHES, default "shake128-rejection", "shake256-wide" for the ciphertexts of versions <= 0.3.0; a public parameter). The keys bound_u and label_modulus of versions <= 0.2.0 are ignored.

Return type:

None

setup()[source]

Sample Z_{i,j} = (s, t) for every client slot.

Return type:

None

get_public_parameters()[source]

Return serialisable public parameters.

Return type:

dict

get_private_keys(nid, **kwargs)[source]

Return the encryption key {z, label_hash} of client nid (None if unknown).

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

Derive the functional key sk_y = sum y_{i,j} Z_{i,j}.

Parameters:
  • sid (str) – Session identifier (unused).

  • **kwargs – Must include credentials with fusion_weight (dict mapping client IDs to integer weight lists).

Return type:

dict | None

Returns:

Dict with sk_y, one vector aggregated over all clients.

Raises:
  • FEKeyError – If credentials are missing.

  • FEValidationError – If a client id is unknown or a weight is malformed or exceeds bound_y.

class pyfe4ai.schemes.mcfe.lwe.MCFELWE(config, **kwargs)[source]

Bases: IPFEAbsCrypto

Crypto operations for LWE-based multi-client inner-product FE (ABMPSW20 Fig. 4).

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise with keys.pp and, for clients, keys.sk.

Parameters:

config (dict)

Return type:

None

encrypt(lst_pt, label)[source]

Encrypt one client’s integer vector under a label.

Parameters:
  • lst_pt (list) – Integer plaintext vector of length at most eta.

  • label (str) – Encryption label; a client must not encrypt twice under the same label.

Return type:

dict

Returns:

Dict with c (one element of Z_q0 per slot).

Raises:
  • FEKeyError – If public parameters or encryption keys are missing.

  • FEValidationError – If the plaintext is malformed or out of bound.

decrypt(dct_ct, dk, fusion_weight, label, _in_layer=False)[source]

Recover sum_i <x_i, y_i> from all clients’ ciphertexts under one label.

Parameters:
  • dct_ct (dict) – Mapping of client IDs to ciphertext dicts.

  • dk (dict) – Functional key {sk_y} derived for fusion_weight.

  • fusion_weight (dict) – Mapping of client IDs to weight lists.

  • label (str) – The encryption label.

  • _in_layer (bool, default: False)

Returns:

The inner product, or None if the decoded value is outside [-n X Y, n X Y]. A wrong label or key yields an arbitrary value in that range rather than an error.

Raises:
encrypt_lst_ndarray(lst_ndarray, **kwargs)[source]

Encrypt ndarrays element-wise (eta = 1), one derived label per element.

Parameters:
  • lst_ndarray (list) – List of numpy arrays.

  • **kwargs – Must include label.

Return type:

list | None

compute_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Aggregate-decrypt ndarray ciphertexts (delegates to decrypt_lst_ndarray_ct()).

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to ciphertext ndarray lists.

  • **kwargs – Must include dk, fusion_weight and label.

Return type:

list | None

decrypt_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Decrypt ndarray ciphertexts element-wise.

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to ciphertext ndarray lists.

  • **kwargs – Must include dk, fusion_weight and label.

Return type:

list | None

Fully-Secure LWE

LWE-based multi-client inner-product FE, adaptive-security parameters.

Abdalla, Bourse, Marival, Pointcheval, Soleimanian, Waldner:
“Multi-Client Inner-Product Functional Encryption in the Random-Oracle Model”
SCN 2020, https://eprint.iacr.org/2020/788 – Fig. 4 with the adaptive
parameter setting of Appendix C.3 (Lemma 33).
The construction is the MCFE counterpart of the fully secure LWE IPFE of
Agrawal, Libert, Stehle (CRYPTO 2016).

The adaptive setting requires m0 * log2(4/3) >= log2(q) + 2 * kappa + 4 * n * log2(P).

  • type: secret-key multi-client encryption (MCFE, LWE, random oracle)

  • setting: Integer based (Z_q with a rounding map to Z_q0)

The algorithms are those of pyfe4ai.schemes.mcfe.lwe; only the choice of m0 differs (larger, so that the statistical step of the proof also covers adaptively chosen challenges). See that module for the construction, the signed-message deviation and the one-ciphertext-per-label rule.

class pyfe4ai.schemes.mcfe.fullysec_lwe.MCFEFullySecLWEKeyGenerator(config, **kwargs)[source]

Bases: MCFELWEKeyGenerator

Key generator for the adaptively secure parameter set of ABMPSW20 Fig. 4.

Parameters:

config (dict)

class pyfe4ai.schemes.mcfe.fullysec_lwe.MCFEFullySecLWE(config, **kwargs)[source]

Bases: MCFELWE

Crypto operations for ABMPSW20 Fig. 4 (adaptive parameters).

Parameters:

config (dict)

LWE Decentralized

Decentralised multi-client inner-product FE over LWE.

Abdalla, Bourse, Marival, Pointcheval, Soleimanian, Waldner:
“Multi-Client Inner-Product Functional Encryption in the Random-Oracle Model”
SCN 2020, https://eprint.iacr.org/2020/788 – Appendix D.1, Fig. 14
(DMCFE from MCFE), instantiated with MCFE1 = their LWE MCFE (Fig. 4,
pyfe4ai.schemes.mcfe.lwe) and MCFE2 = their DCR MCFE (Fig. 3,
  • type: decentralised multi-client encryption (DMCFE, LWE + DCR, random oracle)

  • setting: Integer based (Z_q with a rounding map to Z_q0; Z_{N2^2}^* for key shares)

Encryption and decryption are those of the LWE MCFE. For weights y every client i publishes an MCFE2 encryption of its contribution sum_j y_{i,j} Z_{i,j} over the integers (a vector of length n0 + m0) under the label H(y); the zero-sum MCFE2 keys make the product of all clients’ shares decrypt to sk_y = sum_{i,j} y_{i,j} Z_{i,j} in Z^(n0 + m0), the functional key of Fig. 4. Versions before this one reduced each contribution mod q, so the decrypted integer sum also told how many y_i t_i were negative in every coordinate. Every client must publish a share. Secure against static corruptions (Theorem 38). Versions <= 0.2.0 used an ad-hoc label scalar with per-client key components instead. A key share costs n0 + m0 DCR encryptions of one label hash (computed with a fixed-base method).

N2 only has to hold the shares (2 n2 X2^2 < N2, a few hundred bits for X2 = q); under the standard security profile each of its primes has at least STANDARD_DCR_PRIME_BITS = 1536 bits. Key derivation thus also rests on DCR, which is not post-quantum, whatever the LWE parameters.

class pyfe4ai.schemes.mcfe.lwe_decentralized.DecentralizedMCFELWEKeyGenerator(config, **kwargs)[source]

Bases: MCFELWEKeyGenerator, ParameterCacheMixin

Sets up LWE MCFE client keys and zero-sum MCFE2 keys (ABMPSW20 Fig. 14).

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the key generator.

Parameters:

config (dict) – As for MCFELWEKeyGenerator (n, lst_nid, eta, lwe_n, bound_x, bound_y, stat_param).

Return type:

None

setup()[source]

Sample the LWE MCFE client keys and the zero-sum MCFE2 keys.

Return type:

None

get_public_parameters()[source]

LWE MCFE public parameters plus n2_mod and lst_nid.

Return type:

dict

get_private_keys(nid, **kwargs)[source]

Return client nid’s keys: LWE MCFE z and MCFE2 s2.

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

Not available: functional keys are derived by the clients.

Parameters:

sid (str)

Return type:

dict | None

class pyfe4ai.schemes.mcfe.lwe_decentralized.DecentralizedMCFELWE(config, **kwargs)[source]

Bases: MCFELWE

LWE MCFE encryption / decryption with decentralised key derivation.

Parameters:

config (dict)

derive_function_decryption_key_share(fusion_weight)[source]

Publish this client’s encrypted contribution sum_j y_{i,j} Z_{i,j} (over Z).

Parameters:

fusion_weight (dict) – The whole weight mapping y (absent clients contribute zero).

Return type:

dict

Returns:

Dict with c (MCFE2 ciphertexts), nid and weights_id.

combine_function_decryption_key_share(dct_dk_shares)[source]

Combine every client’s share into the LWE MCFE key {sk_y}.

Raises:

FESchemeError – If a client’s share is missing or the shares were derived for different weights.

Parameters:

dct_dk_shares (dict)

Return type:

dict

LWE Threshold

Threshold LWE-based multi-client inner-product FE: ABMPSW20 LWE MCFE with a Shamir-shared functional key and noise-flooded partial decryptions.

MCFE layer: Abdalla, Bourse, Marival, Pointcheval, Soleimanian, Waldner,
“Multi-Client Inner-Product Functional Encryption in the Random-Oracle
Model”, SCN 2020, https://eprint.iacr.org/2020/788, Fig. 4 (see
Threshold layer in the setting of R. Xu et al., “TAPFed: Threshold Secure
Aggregation for Privacy-Preserving Federated Learning”, IEEE TDSC 2024:
the aggregated key is Shamir-shared among s servers, any t decrypt.
  • type: secret-key multi-client encryption, threshold functional decryption

  • setting: Integer based (Z_q with a rounding map to Z_q0), random oracle

Construction (notation of pyfe4ai.schemes.mcfe.lwe):

  • KeyGen: sk_y = sum y_{i,j} Z_{i,j} mod q; every coordinate is Shamir-shared with threshold t over Z_q (q prime) at points 1..s; server k receives sk_y^(k).

  • ShareDec: server k of an enrolled set E publishes w_k = lambda_k <sk_y^(k), H(l)> + e_k mod q with lambda_k its Lagrange coefficient for E and flooding noise e_k <- D_{Z, sigma_f} (truncated at 12 sigma_f), plus sum y_{i,j} c_{i,j} mod q0 for a consistency check. e_k is derived from a per-server secret key, l, lambda_k and the key share’s pp_binding (the public parameters it is bound to), so requests for the same label and coefficient get the same answer.

  • Combine: v = sum_{k in E} w_k = <sk_y, H(l)> + sum e_k mod q; decode mu' = sum y c - round_q0(v) as in the MCFE.

Why the noise: without e_k a partial decryption is an exact linear function of the server’s share, so n0 + m0 partial decryptions of one server reveal its share and, with t shares, sk_y; even the combined values <sk_y, H(l)> over n0 + m0 labels reveal sk_y. With the noise, a server’s answers are LWE samples (H(l), <lambda_k sk_y^(k), H(l)> + e_k). If fewer than t - 1 shares are corrupted the share is uniform; once t - 1 are, an honest answer is equivalent to an LWE sample in sk_y, whose last m0 coordinates are small (Gaussian of width sigma_t) rather than uniform. The combined values are LWE samples in sk_y as well. This is an LWE-based (computational) argument for dimension n0 + m0, modulus q and width sigma_f; it is our own and not covered by the cited papers, which is why the scheme’s status is experimental.

Noise is derandomised because fresh noise per request would let a combiner average many answers to the same (l, E). Answers for different enrolled sets containing server k carry independent noise; since the Lagrange coefficients are small rationals, r such sets allow averaging that reduces the noise by at most sqrt(r).

Sharing (config entry sharing; default "replicated" for t < s and "shamir" for t = s): the argument above takes one answer carrying <sk_y, H(l)>, with multiplier 1, per label and enrolled set. With "shamir" and t < s (versions <= 0.3.0) that is not what a coalition C of t - 1 servers sees: an honest answer is computable from C’s shares up to M_k <sk_y, H(l)> + e_k, M_k = lambda_k^E prod_{j in C} (k - j) / (0 - j), and several honest answers of one enrolled set carry multipliers other than 0 and 1 (rationals r_k / d_k, or integers such as -3 and 4). Then d_k (w_k - known) - r_k v, with v the combined value, is the small integer d_k e_k - r_k sum_E e_j: the answers fix the noise terms up to multiples of D M (D the common denominator; sum_E e_j modulo D), and v is an LWE sample whose error is D |M|_2 times narrower than the sigma_f of one answer of multiplier 1: sqrt(10) for s = 3, t = 2, C = {2}, E = {1, 3} (M = (3/4, 1/4)), up to 5 for s = 3, t = 2 and 79 for s = 5, t = 3 per enrolled set, and 9.1 and 869 over all enrolled sets of one label. This is not a break, but the argument does not cover it. "replicated" (see share_replicated()) makes every M_k 0 or 1, with one honest answer of multiplier 1 per enrolled set; the other answers are computable up to their own noise. sigma_f and q are unchanged. With t = s both are as described. An explicit "shamir" with t < s is refused under the standard profile and draws an FESecurityWarning under toy (this scheme caches no parameters). Servers refuse Shamir key shares with t < s unless created with legacy_shamir = "warn" (never with public parameters that record the standard profile); key shares of versions <= 0.3.0 are refused in any case (another flooding derivation, see check_flooding_version()); public parameters record threshold_construction and security_profile, and servers refuse constructions they do not know. The opt-in "replicated-seeded" (see share_replicated_seeded()) deals the same kind of pieces, all but one expanded from 32-byte seeds, with the partial decryptions of the replicated key share of the expanded pieces; the key shares hide sk_y computationally (SHAKE-256 keyed by the seeds as a PRF) rather than perfectly.

Statistical smudging in the usual sense (noise hiding a small ciphertext error) does not apply, because these ciphertexts carry no LWE error, only rounding. sigma_f is the largest width that keeps decryption correct: (q0 / q) * s * 12 sigma_f <= (n Y + 1) / 2 uses the half of the error budget that pyfe4ai.primitives.lattice.lwe_utils.abmpsw20_lwe_parameters() reserves for threshold decryption, which gives a noise-to-modulus ratio of roughly 1 / (q0 s).

pyfe4ai.schemes.mcfe.lwe_threshold.flooding_sigma(params, n_slots, bound_y, s)[source]

Largest sigma_f with (q0 / q) * s * c * sigma_f <= (n Y + 1) / 2.

Parameters:
  • params (dict) – Output of abmpsw20_lwe_parameters().

  • n_slots (int) – Total number of slots n.

  • bound_y (int) – Weight bound Y.

  • s (int) – Number of decryption servers (upper bound on |E|).

Return type:

int

class pyfe4ai.schemes.mcfe.lwe_threshold.ThresholdMCFELWEKeyGenerator(config, **kwargs)[source]

Bases: MCFELWEKeyGenerator

Key generator for threshold ABMPSW20 LWE MCFE.

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the key generator.

Parameters:

config (dict) – As for MCFELWEKeyGenerator, plus s (number of decryption servers), t (threshold), lst_sid and sharing ("replicated", the default for t < s, "shamir", the default for t = s, or the opt-in "replicated-seeded"; see resolve_sharing()). flooding_accounting must be the default: the flooding here is part of an LWE argument, not a statistical step that a KL bound could account for.

Return type:

None

setup()[source]

Sample the client keys and forget any previously shared keys.

Return type:

None

get_public_parameters()[source]

Return the MCFE public parameters plus s, t, lst_sid, sigma_f, sharing, threshold_construction and security_profile.

Return type:

dict

get_decryption_keys(sid, **kwargs)[source]

Return server sid’s share of sk_y for the given weights.

All servers asking for the same fusion weights receive shares of one and the same sharing; the key is label-independent (a label in the credentials is ignored).

Parameters:
  • sid (str) – Decryption-server identifier.

  • **kwargs – Must include credentials with fusion_weight.

Return type:

dict | None

Returns:

Dict with sk_y (this server’s share), sid, lst_sid, weights_id (digest of the weights) and noise_key (this server’s secret key for deriving flooding noise), or None if sid is unknown. With replicated sharing sk_y is replaced by sharing and pieces ([{"set": T, "share": {"sk_y": ...}}]); with "replicated-seeded" by sharing, pieces (the correction piece, or nothing), seeds and layout.

class pyfe4ai.schemes.mcfe.lwe_threshold.ThresholdMCFELWE(config, **kwargs)[source]

Bases: MCFELWE

Crypto operations for threshold ABMPSW20 LWE MCFE.

A server refuses Shamir key shares with t < s (versions <= 0.3.0) unless its config has legacy_shamir = "warn" (see LEGACY_SHAMIR_POLICIES).

Parameters:

config (dict)

share_decrypt(dict_ct, credentials, dk, lst_sid_enrolled, _share=None)[source]

Compute this server’s noise-flooded partial decryption.

Parameters:
  • dict_ct (dict) – Mapping of client IDs to ciphertexts under one label.

  • credentials (dict) – Dict with fusion_weight (the weights the key was derived for) and label.

  • dk (dict) – This server’s key share.

  • lst_sid_enrolled (list) – The servers whose partial decryptions will be combined (at least t, including this one).

Return type:

dict

Returns:

Dict with w, c_y, label, sid and lst_sid_enrolled.

Raises:

FESchemeError – If the enrolled set is invalid or the weights are not those the key share was derived for.

combine_decrypt(partials)[source]

Combine partial decryptions from at least t servers.

Parameters:

partials – {sid: share_decrypt(...)} for one enrolled set, or a list of such mappings for several enrolled sets, whose results must then agree (see combine_consistently(): servers are assumed honest-but-curious, and only this redundancy detects one that returns a wrong partial).

Raises:

FESchemeError – If a group is incomplete or invalid, its partial decryptions disagree on the ciphertexts they decrypted, or the groups disagree.

Return type:

float | None

compute_lst_ndarray_ct(dict_ndarray_ct, credentials, dk, lst_sid_enrolled)[source]

Partially decrypt every element of clients’ ndarray ciphertexts.

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to ciphertext ndarray lists produced by encrypt_lst_ndarray() under credentials["label"].

  • credentials (dict) – Dict with fusion_weight and label.

  • dk (dict) – This server’s key share.

  • lst_sid_enrolled (list) – The servers whose shares will be combined.

Return type:

list

decrypt_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Combine element-wise partial decryptions from at least t servers.

Parameters:

dict_ndarray_ct (dict) – Mapping of server ids to the outputs of compute_lst_ndarray_ct().

Raises:

FESchemeError – If the servers’ outputs differ in structure, an element does not decrypt (out of range), or the partial decryptions of an element disagree on the ciphertexts (the error and message of combine_decrypt).

Return type:

list

Ring-LWE

Ring-LWE identity-based multi-client inner-product FE.

Bermudo Mera, Karmakar, Marc, Soleimanian:
“Efficient Lattice-Based Inner-Product Functional Encryption”, PKC 2022
(ePrint 2021/046) – Section 6, “MCFE-Compiler” (from multi-input to
identity-based multi-client IPFE, random oracle; Theorem 5, static
corruptions) applied to the MIFE of pyfe4ai.schemes.mife.ring_lwe.
  • type: identity-based multi-client encryption (MCFE, Ring-LWE, random oracle)

  • setting: R_q = Z_q[x] / (x^ring_n + 1)

Construction (MIFE notation; H(u'_i, l) in R_q^eta is a hash keyed by the client secret u'_i):

  • Setup: MIFE setup plus a secret u'_i per client.

  • Encrypt: ct_{i,l} = MIFE.Enc(msk_i, x_i + H(u'_i, l)), i.e. the slot encrypts x_i + u_i + H(u'_i, l).

  • KeyGen: for weights y and label l: the MIFE key ({sk_{i,y}}, z) with z'' = z + sum_i y_i H(u'_i, l).

  • Decrypt: MIFE.Dec with z''.

Functional keys are bound to a label (identity-based MCFE): a key for (y, l) only decrypts ciphertexts produced under l. Each client must encrypt at most once per label. The ndarray helpers pack values into the ring_n coefficients of one ciphertext per chunk; chunk c of label l uses H(u'_i, (l, c)), and a key for n_chunks chunks carries one z'' per chunk. H is SHAKE-256 over the client secret and the encoded label (hash_to_zq_vector()), mapped to Z_q as the key generator’s label_hash says (a public parameter; LABEL_HASHES): by default rejection sampling mod q from SHAKE128 ("shake128-rejection": exactly uniform, fewer XOF bytes). Versions <= 0.3.0 reduced bits(q) + 128-bit SHAKE-256 blocks ("shake256-wide", still selectable), so their ciphertexts and key terms differ. The mode is bound to everything that depends on it: the public parameters, the clients’ encryption keys, every functional key (and threshold key share), every single ciphertext and every ndarray layer (label_hash: the code of LABEL_HASH_CODES, once per layer dict, not per chunk; covered by the threshold variant’s tags); a missing field means "shake256-wide" (the data of versions <= 0.3.0, which stay readable), and encryption with a key of another mode, or decryption of ciphertexts or with keys of another mode, raises instead of returning wrong sums. Versions <= 0.2.0 masked inputs with a small public label scalar.

pyfe4ai.schemes.mcfe.ring_lwe.label_pad(u_prime, label, q, eta, ring_n, mode)[source]

Return H(u'_i, label) as eta rows of ring_n coefficients mod q (mode: the label_hash public parameter; expanded by the backend’s hash_to_zq(), natively when the extension has a context for (q, ring_n)).

Parameters:
Return type:

list

pyfe4ai.schemes.mcfe.ring_lwe.label_pad_material(u_prime, label, mode)[source]

The hash input of label_pad() (the native kernels expand it themselves).

Parameters:
Return type:

bytes

pyfe4ai.schemes.mcfe.ring_lwe.chunk_key_index(layer, j, n_keys)[source]

Index of the chunk key for chunk j of an ndarray ciphertext layer.

Raises:

FESchemeError – Unless the layer’s first_chunk is a non-negative integer and all its chunks have a key among the n_keys.

Parameters:
Return type:

int

class pyfe4ai.schemes.mcfe.ring_lwe.MCFERingLWEKeyGenerator(config, **kwargs)[source]

Bases: MIFERingLWEKeyGenerator

Key generator for BMMS21 identity-based MCFE over Ring-LWE.

Parameters:

config (dict)

__init__(config, **kwargs)[source]

As MIFERingLWEKeyGenerator, plus label_hash (one of LABEL_HASHES, default "shake128-rejection"; "shake256-wide" for the ciphertexts of versions <= 0.3.0), published in the public parameters.

Parameters:

config (dict)

Return type:

None

get_public_parameters()[source]

MIFE public parameters plus label_hash.

Return type:

dict

setup()[source]

MIFE setup plus a secret u'_i for every client.

Return type:

None

get_private_keys(nid, **kwargs)[source]

Return client nid’s keys: MIFE pk, u and the label-pad secret u_prime.

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

Derive the key for weights y and label l (identity-based).

Parameters:
  • sid (str) – Session identifier (unused).

  • **kwargs – credentials with fusion_weight, label and optionally n_chunks (number of ndarray chunks to cover).

Return type:

dict | None

Returns:

Dict with sk_y (per-slot MIFE keys), z (pad term of the label itself), z_chunks (pad terms of chunks 0..n_chunks-1) and label.

class pyfe4ai.schemes.mcfe.ring_lwe.RingLWEPrecomputation(owner, label, shapes, first_chunks, native=None, rows=None)[source]

Bases: OneTimePrecomputation

Offline phase of MCFERingLWE.encrypt_lst_ndarray() for one label and fixed layer shapes (see MCFERingLWE.precompute()). Secret and one-time (pyfe4ai.core.one_time).

Parameters:
class pyfe4ai.schemes.mcfe.ring_lwe.MCFERingLWE(config, **kwargs)[source]

Bases: MIFERingLWE

Crypto operations for BMMS21 identity-based MCFE over Ring-LWE.

Parameters:

config (dict)

property label_hash: str

The label_hash public parameter (see pp_label_hash()).

encrypt(matrix_pt, label)[source]

Encrypt this client’s matrix under label (once per client and label).

Parameters:
Return type:

dict

decrypt(dct_ct, dk, fusion_weight, label)[source]

Recover the batch of inner products for a key bound to label.

Raises:

FESchemeError – If the key was derived for another label or the ciphertexts do not cover exactly the key’s clients.

Parameters:
static key_chunks(lst_ndarray_ct)[source]

Number of chunk keys needed for the output of encrypt_lst_ndarray().

Parameters:

lst_ndarray_ct (list)

Return type:

int

encrypt_lst_ndarray(lst_ndarray, **kwargs)[source]

Pack ndarrays into ring_n-coefficient chunks (eta = 1).

Chunk c (counted across all layers) is encrypted under the chunk label (label, c), so each chunk has its own label pad.

Parameters:
  • lst_ndarray (list) – The layers.

  • **kwargs – label (required) and optionally precomputed, the result of precompute() for this label and these layer shapes (the online phase; one-time).

Return type:

list | None

Returns:

One dict per layer with shape, first_chunk and chunks.

precompute(label, shapes)[source]

Offline phase of encrypt_lst_ndarray() for layers of these shapes.

Everything of the ciphertexts but the plaintext term – label pads, Gaussian samples, pk r + e_0 + Delta (u + H) and a r + e per chunk – is computed now; the online phase, encrypt_lst_ndarray(layers, label=label, precomputed=...), only adds Delta x. The ciphertexts are those of a direct encrypt_lst_ndarray() with the same samples.

The result determines the encryption randomness: keep it as secret as the key, and use it once (an MCFE client encrypts at most once per label). The use is atomic, and copy.copy, copy.deepcopy, pickling and use in another process (after os.fork()) are refused (pyfe4ai.core.one_time). Natively each chunk is overwritten in memory when used; on the Python path the references to the chunks are dropped (Python integers cannot be wiped).

Parameters:
  • label (str) – The label the layers will be encrypted under.

  • shapes – One shape (tuple of ints, or an int for a 1-D layer) per layer.

Return type:

RingLWEPrecomputation

compute_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Aggregate-decrypt packed ndarray ciphertexts (needs dk, fusion_weight, label).

Parameters:

dict_ndarray_ct (dict)

Return type:

list | None

decrypt_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Decrypt packed ndarray ciphertexts with a key covering their chunks.

Parameters:

dict_ndarray_ct (dict)

Return type:

list | None

Ring-LWE Decentralized

Ring-LWE identity-based decentralised multi-client inner-product FE.

Bermudo Mera, Karmakar, Marc, Soleimanian:
“Efficient Lattice-Based Inner-Product Functional Encryption”, PKC 2022
(ePrint 2021/046) – Section 6: the identity-based MCFE of
pyfe4ai.schemes.mcfe.ring_lwe, decentralised with the compiler of
Abdalla, Benhamouda, Kohlweiss, Waldner (PKC 2019) extended by a
“generalized distributed sum” (GDSum) of label-dependent functions
H_i(l) with sum_i H_i(l) = 0.
  • type: identity-based decentralised multi-client encryption (Ring-LWE, ROM)

  • setting: R_q = Z_q[x] / (x^ring_n + 1)

Encryption and decryption are those of the identity-based MCFE. There is no authority for functional keys: for weights y and label l client i publishes

  • its per-slot IPFE key sum_row y_{i,row} s_{i,row} (part of the key), and

  • z_i = sum_row y_{i,row} (u_{i,row} + H(u'_i, l)_row) + M_i(l, y),

where the GDSum masks M_i(l, y) = sum_{j > i} PRF(K_ij, l, y) - sum_{j < i} PRF(K_ji, l, y) cancel over all clients, so sum_i z_i is the pad term z'' of the key and each z_i alone is uniformly masked. Every client must publish a share; shares for other weights or labels do not combine.

Deviation: the PKC version of BMMS21 states GDSum only through its property; this module realises it with pairwise PRF masks (SHAKE-256 keyed by pairwise secrets K_ij, which this prototype samples in-process, as it does all other keys; a deployment would agree on them by key exchange), hashed to Z_q like the label pads (the label_hash public parameter). Versions <= 0.2.0 used an ad-hoc label scalar instead.

pyfe4ai.schemes.mcfe.ring_lwe_decentralized.gdsum_material(pair_key, context, mode)[source]

The hash input of one PRF term of gdsum_mask() (mode: the label_hash public parameter).

Parameters:
Return type:

bytes

pyfe4ai.schemes.mcfe.ring_lwe_decentralized.gdsum_mask(nid, pair_keys, lst_nid, context, q, ring_n, mode)[source]

Return M_i(context) = sum_{j>i} PRF(K_ij) - sum_{j<i} PRF(K_ji) in Z_q^ring_n (the PRF: hash_to_zq_vector() with the label_hash mode).

Parameters:
Return type:

list

class pyfe4ai.schemes.mcfe.ring_lwe_decentralized.DecentralizedMCFERingLWEKeyGenerator(config, **kwargs)[source]

Bases: MCFERingLWEKeyGenerator

Sets up identity-based MCFE client keys and pairwise GDSum secrets.

Parameters:

config (dict)

setup()[source]

MCFE setup plus a secret K_ij shared by every pair of clients.

Return type:

None

get_public_parameters()[source]

MCFE public parameters plus lst_nid.

Return type:

dict

get_private_keys(nid, **kwargs)[source]

Client nid’s MCFE keys, its IPFE secret s and pairwise pair_keys.

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

Not available: functional keys are derived by the clients.

Parameters:

sid (str)

Return type:

dict | None

class pyfe4ai.schemes.mcfe.ring_lwe_decentralized.DecentralizedMCFERingLWE(config, **kwargs)[source]

Bases: MCFERingLWE

Identity-based MCFE encryption / decryption with GDSum key derivation.

Parameters:

config (dict)

derive_function_decryption_key_share(fusion_weight, label, n_chunks=0)[source]

Publish this client’s key share for weights y and label l.

Parameters:
  • fusion_weight (dict) – Weights of every client (eta integers each).

  • label (str) – The label the key will be bound to.

  • n_chunks (int, default: 0) – Number of ndarray chunks the key must cover.

Return type:

dict

Returns:

Dict with sk_y (this slot’s IPFE key), z and z_chunks (masked pad terms), label, nid and weights_id.

Raises:

FEKeyError – Without the client’s keys, or if u or s has fewer than eta rows of ring_n values.

combine_function_decryption_key_share(dct_dk_shares)[source]

Sum every client’s share into the identity-based MCFE key.

Raises:
  • FESchemeError – If a client’s share is missing or the shares were derived for different weights, labels or chunk counts.

  • FEKeyError – If a share’s sk_y, z or a z_chunks entry is not ring_n values.

Parameters:

dct_dk_shares (dict)

Return type:

dict

Ring-LWE Threshold

Threshold Ring-LWE identity-based multi-client inner-product FE: the BMMS21 identity-based MCFE with a Shamir-shared functional key and statistically flooded partial decryptions.

MCFE layer: pyfe4ai.schemes.mcfe.ring_lwe (BMMS21, PKC 2022, Section 6).
Threshold layer in the setting of R. Xu et al., “TAPFed: Threshold Secure
Aggregation for Privacy-Preserving Federated Learning”, IEEE TDSC 2024.
  • type: identity-based multi-client encryption, threshold functional decryption

  • setting: R_q = Z_q[x] / (x^ring_n + 1), q a product of NTT-friendly primes (default) or an NTT-friendly prime

Defaults (both security profiles, standard and toy): binary ciphertexts, key shares and partial decryptions (ct_format="binary"), an RNS modulus (ring_modulus="rns") and key shares split into a weight part per weights and a label part per label (key_shares="split"; whole key shares for the replicated-seeded sharing). Each is selectable as before – ct_format="json", ring_modulus="prime", key_shares="full" or key_part="full" per call – and public parameters, parameter records, ciphertexts and key shares of the earlier defaults (no ct_format: JSON; no q_factors: a prime) keep working where they did. The defaults change no construction id; their parameter records are param-rns.json (a folder’s param.json is the prime’s). What does not match fails closed: a server refuses ciphertexts and key shares of the other format or of other public parameters (pp_binding), and a label part without its weight part (ThresholdMCFERingLWE.add_weight_key_share()); a combiner refuses partial decryptions of the other format (partial decryptions carry no digest of the public parameters; servers are honest-but-curious, see below) and partial decryptions of different sharings of one key (MixedSharingsError).

Construction (notation of pyfe4ai.schemes.mife.ring_lwe):

  • KeyGen: the identity-based key ({sk_i}, z'', z''_chunks) for (y, l); every coefficient is Shamir-shared with threshold t over Z_q.

  • ShareDec: server k of an enrolled set E publishes, coefficient-wise, w_k = lambda_k (sum_i ct1_i sk_i^(k) + Delta z''^(k)) + e_k and c_y = sum_i sum_row y_row ct0_{i,row}.

  • Combine: c_y - sum_{k in E} w_k = Delta <x, y> + noise - sum e_k; decode.

Flooding: the exact key part differs from what the result and the ciphertexts determine by the decryption noise sum_row y_row (e'_row r + e_row) - e sum_row y_row s_row, bounded per coefficient by B = n eta Y (2 ring_n 144 sigma1 sigma2 + 12 sigma3); flooding noise e_k per coefficient smudges it (statistical argument; uniform by default, Gaussian D_{Z, 2^kappa B} selectable, see below), with q enlarged so that s flooding terms still decode. Ciphertexts (and ndarray chunks) carry one HMAC tag per server over the (chunk) label and the ciphertext (see “Ciphertext format” below); servers only decrypt ciphertexts tagged for them under the label they serve, since w_k is linear in ct1. The noise is seeded with a per-server key (fixed per weights and label), lambda_k, the label or chunk, the ct1 values and the key share’s pp_binding (the public parameters it is bound to), so a request for the same key part draws the same noise; a corrupted client varying its own ct1 draws fresh noise each time. The flooding argument is statistical per answer (a statistical distance of about 2^-stat_param per coefficient, adding up over the answers); it does not bound what many answers to varied ct1 reveal together: averaging them would take about 2^(2 stat_param) partial decryptions with Gaussian noise (see below for the uniform default), but other ways of combining these noisy linear equations are not ruled out. The threshold layer is our own design.

Flooding distribution (key generator config flooding, a public parameter):

  • "uniform" (default): e_k uniform on [-2^tau, 2^tau) with tau = bits(B) + kappa, read from the same seeded XOF (uniform_flooding_poly()). By the smudging lemma (Asharov et al., EUROCRYPT 2012, Lemma 2.1) the statistical distance per coefficient is |E| / 2^(tau + 1) < 2^-(kappa + 1), the per-sample bound the Gaussian meets as well; q holds s 2^tau instead of s 12 sigma_f (about 3 bits less), correctness needs no tail cut, and the sampler is exact and integer-only (no mpfr). Unlike the Gaussian it has no margin beyond that bound: over N coefficients the distance is N 2^-(kappa + 1) (the Gaussian’s is sqrt(N) times its per-sample bound), and telling E from E + 1 by repetition takes about 2^(tau + 1) partial decryptions instead of 2^(2 kappa) B^2. Parameter records bmms21-ibmcfe-shamir-smudging-uniform/v1; public parameters flooding and flood_bits.

  • "gaussian": e_k <- D_{Z, 2^kappa B} as described above, the only flooding of versions <= 0.3.0 (public parameter sigma_f; public parameters without flooding are read as this), drawn by the pinned FLOODING_SAMPLER at bits(sigma_f) + 320 bits (records bmms21-ibmcfe-shamir-smudging/v2; the v1 records of earlier versions are regenerated).

Decryption key shares record the flooding they were issued for (flooding, with noise_version and noise_sampler); a server refuses a share of another flooding than its public parameters’, which would answer a request with a second, independent noise.

Sharing (config entry sharing; default "replicated" for t < s and "shamir" for t = s): with "shamir" and t < s – the sharing of versions <= 0.3.0 – the smudging argument does not hold: with t - 1 servers C corrupted an honest partial is computable from C’s shares and the result up to e_k - M nu per coefficient, M = lambda_k^E prod_{j in C} (k - j) / (0 - j), a non-integer rational r / d for some (C, E, k), and d times the difference reveals nu mod d coefficient by coefficient, whatever sigma_f. Fewer corrupted servers, or none, can suffice (the aggregator alone reads nu modulo (s - 1)! for t = 1), and combining several enrolled sets raises d (144 for s = 5, t = 3); integer M reach 4 for s = 3, t = 2, which multiplies the statistical distance of the flooding by 4. "replicated" (see share_replicated()) makes M 0 or 1, so the argument holds as stated with q and ring_n unchanged; a server holds C(s - 1, t - 1) key pieces instead of one share (dealt and added up natively where the extension is built, with the values of the Python code). With t = s both are sound. An explicit "shamir" with t < s is refused under the standard profile and draws an FESecurityWarning under toy; the replicated sharing has its own parameter records (threshold_construction bmms21-ibmcfe-replicated-smudging-uniform/v1, or bmms21-ibmcfe-replicated-smudging/v1 with the Gaussian flooding), so a record cached by an earlier version for t < s is not reused by the default. Servers refuse Shamir key shares with t < s unless created with legacy_shamir = "warn" (never with public parameters that record the standard profile); key shares of versions <= 0.3.0 are refused in any case (another flooding derivation, see check_flooding_version()); public parameters record threshold_construction and security_profile, and servers refuse constructions they do not know. The opt-in "replicated-seeded" (see share_replicated_seeded()) deals the same kind of pieces, all but one expanded from 32-byte seeds: a server stores seeds and at most one full piece (the size of a Shamir key share), and its partial decryptions are those of the replicated key share of the expanded pieces; the key shares hide the key computationally (SHAKE-256 keyed by the seeds as a PRF) rather than perfectly. It has its own records (bmms21-ibmcfe-replicated-seeded-smudging-uniform/v1, or bmms21-ibmcfe-replicated-seeded-smudging/v1 with the Gaussian flooding).

flooding_accounting = "kl" with max_partial_decryptions = Q (opt-in, t = s with the Shamir sharing and the Gaussian flooding only – the flooding defaults to "gaussian" under "kl" and "uniform" is refused there –, see pyfe4ai.schemes.threshold.kl) sizes the Gaussian noise for a run in which every server produces at most Q partial decryptions (one per ciphertext or chunk): KL <= 2^-(stat_param + 1) for the N = s Q ring_n flooded coefficients, i.e. sigma_f = B sqrt(N 2^stat_param), with a tail cut derived from N (16 sigma_f at N = 2^53), instead of a statistical distance 2^-stat_param per coefficient. It is a different, weaker guarantee (stat_param - 6.85 bits in the bit-security sense of Micciancio and Walter, about 2^-65 of total statistical distance at stat_param = 128), valid only for the first Q partial decryptions of every server, which each server counts in the partial_budget its deployment gives it; averaging the noise away would need about N 2^stat_param requests. Under standard (the benchmark’s three clients, X = 100, Y = 1, s = t = 3) N = 2^53 gives a q of 178 bits instead of 215 with the per-sample Gaussian flooding (212 with the uniform default), at the same ring_n 8192. These parameters have their own record (threshold_construction bmms21-ibmcfe-shamir-smudging-kl/v2).

Ciphertext format (key generator config ct_format, a public parameter; CT_FORMATS): "json" (decimal strings, tags over the JSON form, as in versions <= 0.3.0; public parameters without ct_format) or "binary" (the default): format 2 of encode_threshold_ciphertext() – the ct0 rows truncated to the batch length k (decryption reads them coefficient-wise), ct1 whole, every value a ceil(bits(q)/8)-byte word strictly below q – and binary partial decryptions of the first k coefficients (about 71 and 54 bytes per element at 215 bits instead of 225 and 222). Tags (v2) are HMAC-SHA256 under the client-server key of binary_tag_message(): the public parameters’ digest (pp_binding() over tmcfe_ring_lwe_binding_fields()), the (chunk) label, the ciphertext’s label hash code, k and SHA-256 digests of the ct0 and ct1 bytes; the noise is seeded with digests of the ct1 bytes (binary_request_digests()). A format-2 encoding is unique – a value >= q is refused, also under a valid tag – so no client can re-encode one request to draw a second noise for the same key part; the first k noise values are those of the full draw (a prefix of the SHAKE stream for the uniform noise), so answers for another k agree on their common coefficients. The format is bound to the key shares (ct_format enters pp_binding where the public parameters have it; JSON digests are unchanged): a key share is served in one format only. Under binary public parameters JSON ciphertexts, format 1, v1 tags and JSON partial decryptions are refused, and under JSON ones every binary form. A client, server or combiner fixes its format when it is made and refuses to work once the bound fields or the format of its public parameters (the caller’s dict) have changed: one key part, one derivation.

Key shares per round (key generator config key_shares, KEY_SHARES; "split" by default): a key share for (y, l) is the label-independent sk_y and the label’s terms z''_l (and of its chunks). ThresholdMCFERingLWEKeyGenerator.get_weight_key_share() issues the shares of sk_y once per weights (servers keep them: ThresholdMCFERingLWE.add_weight_key_share()), and get_decryption_keys (key_part="label", the default with split key shares) the shares of the label terms only (label_term=False leaves out the label’s own z, which only single ciphertexts need): two of six polynomials per round for two chunks; per round a server needs nothing else from the key generator. key_shares="full" (or key_part="full" per call) issues whole key shares, as in versions <= 0.3.0; the replicated-seeded sharing has only these. Both parts are of one sharing (Shamir points or replicated sets) and carry a share_set_id; a server combines a label part only with the weight part of the same weights, share set, sharing, binding, flooding, noise derivation and MAC keys. A linear sharing of sk_y reused with fresh sharings of the label terms is a sharing of each round’s key, the shares of t - 1 servers stay independent of the keys, and the noise is seeded as for a full key share; NTT(sk_y) of a kept weight part is computed once. With binary public parameters every polynomial of the Shamir and replicated key shares is bytes (ceil(bits(q)/8)-byte words, strictly below q; the seeded sharing keeps decimal strings). Key material mixing both kinds is dealt as values; key shares whose polynomials are neither bytes nor lists or tuples (e.g. memoryview), are of another length, hold values other than ints, mpz and decimal strings, or are replicated pieces mixing kinds, are refused (FEKeyError). The key generator keeps the sharings of sk_y for the last WEIGHT_SHARINGS_KEPT weights: weights issued again after that come with a new share set – their weight part and every label part issued from then on – which servers holding the old weight part refuse until given the new one. A label part is of the weights’ share set current when it is issued (one made or cached while another thread evicted that sharing is made again for the new set) and is kept with that sharing, and dropped with it; per sharing the label parts of the last LABEL_PARTS_KEPT labels are kept (a label asked for again later gets a new sharing of its terms). Every sharing the key generator issues – whole key shares, or the label parts of one label – has a random sharing_id, which the servers’ partial decryptions carry: partials of two sharings of one key would decode to a wrong value inside the decoding bound rather than fail, so combiners refuse a mix (MixedSharingsError; fetch the label parts again for all servers). The two sharings’ answers of one server differ by its share of a sharing of zero, which t - 1 servers compute from their own parts: a second sharing reveals nothing. Store and transmit the sharing_id with every key share and partial decryption: partials without one (as from key shares of earlier versions) combine only with each other, so a storage or transport layer that drops the id on every side would let partials of two sharings combine to a wrong value unnoticed.

The threshold objects (key generator, clients, servers) are not portable across versions of pyfe4ai: a state pickled by another version is refused when loaded (create the object again).

Modulus (key generator config ring_modulus): "rns" (the default): q the product of NTT primes below 2^62, at least MIN_RNS_FACTORS (three: the native threshold kernels run on the 256-bit ring, also for the smaller q of the toy profile) and as many as make each at most 60 bits (four for the 212 to 218 bits of the standard profile, about 54 bits each), but four up to 248 bits, the most the native ring holds (above, five or more on the Python path, with a warning – "prime" stays native below 2^253; rns_ntt_modulus_for()), of the bit length of the prime and above the same bound, published as q_factors (bound to the key shares); or "prime", an NTT prime (the modulus of earlier versions). Every factor must exceed s (check_rns_servers()): Shamir sharing over Z_q is then one field sharing per factor (CRT), with units for points and differences; the replicated sharing, the flooding (an argument over the integers), the label hash (exact rejection sampling on [0, q)) and the decoding bound do not depend on the factorisation (see RNS modulus for the Ring-LWE schemes: security review). The parameter record carries ring_modulus and q_factors (file param-rns.json, beside the prime’s param.json); the construction ids are those of the prime modulus. The native backend runs the 256-bit kernels with one 64-bit NTT per factor (NTT-domain values are residue tuples), with the values of the Python code on the composite q.

With the native backend (pyfe4ai.backend; moduli below 2^253, which includes the flooded q of the standard profile) the MCFE layer, the Shamir sharing of the key, the parsing, digests and NTT-domain products of a partial decryption and the combination run natively, with the values of the Python code; the uniform flooding noise is drawn natively too (uniform_flooding_poly()’s values), the Gaussian by discrete_gaussian_bigint() (natively too, with the same samples).

pyfe4ai.schemes.mcfe.ring_lwe_threshold.MIN_RNS_FACTORS = 3

Fewest factors of an RNS modulus of the threshold scheme. Its native kernels run on the 256-bit ring (two factors below 2^124 would get the two-prime context of the MIFE / MCFE prototype, which has none of them).

class pyfe4ai.schemes.mcfe.ring_lwe_threshold.ThresholdMCFERingLWEKeyGenerator(config, **kwargs)[source]

Bases: MCFERingLWEKeyGenerator

Key generator for threshold BMMS21 identity-based MCFE over Ring-LWE.

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the key generator.

Parameters:

config (dict) – As for MCFERingLWEKeyGenerator, plus s, t, lst_sid, stat_param (kappa of the flooding), flooding (one of FLOODINGS, default "uniform", "gaussian" for the partial decryptions of versions <= 0.3.0 and under "kl"; see the module docstring), sharing ("replicated", the default for t < s, "shamir", the default for t = s, or the opt-in "replicated-seeded"; see resolve_sharing()) and, opt-in, flooding_accounting = "kl" with max_partial_decryptions (t = s, Shamir sharing, Gaussian flooding; see the module docstring); ct_format (one of CT_FORMATS, default "binary", published in the public parameters, whose clients and servers then use format 2; "json" for the format of versions <= 0.3.0); ring_modulus: "rns" (default): the flooded q is a product of at least MIN_RNS_FACTORS NTT primes below 2^62 (four for the 212 to 218 bits of the standard profile, four up to 248 bits; rns_ntt_modulus_for()), each above s, published as q_factors, with the parameter record param-rns.json; or "prime", an NTT prime (record param.json, as in earlier versions); key_shares (one of KEY_SHARES): "split", the default for the Shamir and replicated sharings, or "full", the default (and only choice) for "replicated-seeded" – what get_decryption_keys() issues without a key_part.

Raises:

FEValidationError – For an unknown ct_format, ring_modulus or key_shares, or key_shares="split" with the "replicated-seeded" sharing.

Return type:

None

setup()[source]

MCFE setup and the client-server MAC keys; forgets previously shared keys.

Return type:

None

get_private_keys(nid, **kwargs)[source]

Client nid’s MCFE keys plus mac_keys ({sid: key}) for the ciphertext tags.

Parameters:

nid (str)

Return type:

dict | None

get_public_parameters()[source]

MCFE public parameters plus s, t, lst_sid, flooding, flood_bits (uniform) or sigma_f (Gaussian), sharing, threshold_construction, security_profile, ct_format (only "binary"; absent means "json") and, under "kl", flooding_accounting, max_partial_decryptions, stat_param and flood_tail_cut.

Return type:

dict

get_decryption_keys(sid, **kwargs)[source]

Return server sid’s share of the key for (y, label, n_chunks).

Parameters:
  • sid (str) – The server.

  • **kwargs – credentials with fusion_weight, label, optionally n_chunks and key_part: "label" – only the label’s terms z and z_chunks, the per-round part; the label-independent sk_y comes once per weights from get_weight_key_share() – or "full", a whole key share (default: "label" if the config’s key_shares is "split", the default but for the replicated-seeded sharing, else "full") and, with "label", label_term (default True; False leaves out z, which only single ciphertexts of the label itself need).

Return type:

dict | None

Returns:

Dict with shares sk_y, z, z_chunks and label, sid, lst_sid, weights_id, noise_key and mac_keys ({nid: key}), or None if sid is unknown. With replicated sharing the shares are replaced by sharing and pieces ([{"set": T, "share": {"sk_y", "z", "z_chunks"}}]); with "replicated-seeded" pieces holds the correction piece (or nothing), and seeds ([{"set": T, "seed": hex}]) and layout give the others. A label part has part = "label" and share_set_id (that of the weight part it completes, the weights’ current share set when it is issued) and no sk_y; label parts are kept with that sharing of sk_y (later calls return the same shares, for the last LABEL_PARTS_KEPT labels) and dropped with it. Every key share part of one sharing has its sharing_id (the partial decryptions carry it; combiners refuse partials of different sharings). With binary public parameters (ct_format="binary") every polynomial of the Shamir and replicated sharings is bytes (ceil(bits(q)/8)-byte words, the same values).

Raises:

FEValidationError – For an unknown key_part, a label part with neither z nor chunk keys, or key_part="label" with the "replicated-seeded" sharing.

get_weight_key_share(sid, **kwargs)[source]

Return server sid’s share of the label-independent part of the key for the weights y: the shares of sk_y ({nid: sk_{i,y}}, or replicated pieces of it), issued once per weights (later calls return the same shares), with part = "weights", share_set_id and the fields of get_decryption_keys(). Servers keep it (ThresholdMCFERingLWE.add_weight_key_share()); per label they then need only get_decryption_keys(..., key_part="label").

The shares of sk_y serve every label: a linear sharing reused with fresh sharings of the label terms is a sharing of each round’s key, and t - 1 servers’ shares stay independent of the keys (each sharing has its own randomness); the flooding noise is seeded as before.

Parameters:
  • sid (str) – The server.

  • **kwargs – credentials with fusion_weight.

Raises:

FEValidationError – With the "replicated-seeded" sharing.

Return type:

dict | None

class pyfe4ai.schemes.mcfe.ring_lwe_threshold.ThresholdMCFERingLWE(config, **kwargs)[source]

Bases: MCFERingLWE

Crypto operations for threshold BMMS21 identity-based MCFE over Ring-LWE.

A server refuses Shamir key shares with t < s (versions <= 0.3.0) unless its config has legacy_shamir = "warn" (see LEGACY_SHAMIR_POLICIES).

Parameters:

config (dict)

__init__(config, **kwargs)[source]

As MCFERingLWE; under flooding_accounting = "kl" a server needs partial_budget, the PartialDecryptionBudget that its deployment keeps for it; legacy_shamir as in the class docstring. The ciphertext format is that of the public parameters (ct_format, pp_ct_format()); a ct_format in config must name the same.

Raises:

FEValidationError – For a ct_format other than the public parameters’.

Parameters:

config (dict)

Return type:

None

encrypt(matrix_pt, label)[source]

MCFE encryption under label, tagged for every server (format 2 for binary public parameters).

Parameters:
Return type:

dict

encrypt_lst_ndarray(lst_ndarray, **kwargs)[source]

Packed ndarray encryption; every chunk is tagged under its chunk label.

Parameters:

lst_ndarray (list)

Return type:

list | None

WEIGHT_SHARES_KEPT = 16

Weight key shares a server keeps (add_weight_key_share()).

add_weight_key_share(share)[source]

Keep this server’s weight key share (ThresholdMCFERingLWEKeyGenerator.get_weight_key_share()) for the label key shares (key_part="label") of the same weights and share set; the last WEIGHT_SHARES_KEPT are kept. NTT(sk_y) of the shares is cached across labels.

Raises:

FEKeyError – Unless share is a weight key share of this server.

Parameters:

share (dict)

Return type:

None

has_weight_key_share(dk)[source]

Whether this server keeps the weight key share that the label key share dk completes (same weights and share set): False means add_weight_key_share() first. A full key share needs none.

Parameters:

dk (dict)

Return type:

bool

share_decrypt(dict_ct, credentials, dk, lst_sid_enrolled, _z_share=None, _chunk=None, _sk_hats=None, _share=None)[source]

Compute this server’s flooded partial decryption.

Parameters:
  • dict_ct (dict) – One ciphertext per client, produced under credentials["label"].

  • credentials (dict) – Dict with fusion_weight and label.

  • dk (dict) – This server’s key share (derived for that label).

  • lst_sid_enrolled (list) – The servers whose partials will be combined.

Return type:

dict

Returns:

Dict with w, c_y, k, ct_digest, sid, lst_sid_enrolled and sharing_id (that of the key share, None for key shares without one).

Raises:

FESchemeError – Under flooding_accounting = "kl" also when the server has no partial_budget or its budget is spent (refused requests consume nothing).

combine_decrypt(partials)[source]

Combine partial decryptions from at least t servers.

Parameters:

partials – {sid: share_decrypt(...)} for one enrolled set, or a list of such mappings for several enrolled sets, whose results must then agree (see combine_consistently(): servers are assumed honest-but-curious, and only this redundancy detects one that returns a wrong partial).

Raises:

FESchemeError – If a group is incomplete or invalid, its partial decryptions disagree on the ciphertexts they decrypted, or the groups disagree.

Return type:

list | None

compute_lst_ndarray_ct(dict_ndarray_ct, credentials, dk, lst_sid_enrolled)[source]

Partially decrypt every chunk of the clients’ packed ndarray ciphertexts (under "kl" refused whole unless the budget covers all of them; each partial produced then counts).

Parameters:
Return type:

list

decrypt_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Combine chunk-wise partial decryptions from at least t servers.

Raises:

FESchemeError – If the servers’ outputs differ in structure, an element does not decrypt (out of range), or the partial decryptions of an element disagree on the ciphertexts (the error and message of combine_decrypt).

Parameters:

dict_ndarray_ct (dict)

Return type:

list

Paillier

DCR (Paillier) based multi-client inner-product FE.

Abdalla, Bourse, Marival, Pointcheval, Soleimanian, Waldner:
“Multi-Client Inner-Product Functional Encryption in the Random-Oracle Model”
SCN 2020, https://eprint.iacr.org/2020/788 – Fig. 3 (MCFE based on DCR),
correctness and security in Appendix B.2 (one-IND secure in the ROM).
  • type: secret-key multi-client encryption (MCFE, DCR, random oracle)

  • setting: Integer based (Z_{N^2}^*, N = p * q with safe primes p, q)

Construction (one coordinate j of client i’s vector is one slot; n below is the total number of slots):

  • Setup: safe-prime modulus N; H: labels -> Z_{N^2}^* full-domain hash.

  • KeyGen: s_{i,j} <- D_{Z, sigma} with sigma > sqrt(kappa + 2 n log2(2X)) * N^{5/2} (adaptive setting).

  • Encrypt: c_{i,j} = (1 + N)^{x_{i,j}} * H(l)^{s_{i,j}} mod N^2.

  • KeyDer: k = sum y_{i,j} s_{i,j} over the integers (one aggregated key).

  • Decrypt: C = prod c_{i,j}^{y_{i,j}} * H(l)^{-k}; <x, y> = (C - 1 mod N^2) / N.

Correctness needs |x|, |y| < sqrt(N / 2n) (Fig. 3); the result is returned centred modulo N, so signed inputs are supported. Each client slot must encrypt at most once per label (one-IND security): two ciphertexts of one slot under one label reveal (1 + N)^{x - x'}. The ndarray helpers therefore derive a distinct label per element.

Implementation notes: H is SHAKE-256 expanded to bits(N^2) + 128 bits (the paper’s implementation uses counter-mode SHA-256); D_{Z, sigma} is a rounded continuous Gaussian computed at bits(sigma) + 320 bits of precision (pyfe4ai.primitives.sampling.discrete_gaussian_bigint()). bit_length is the size of each prime and is far below any real security level by default.

pyfe4ai.schemes.mcfe.paillier.dcr_sigma(n_mod, n_slots, bound_x, kappa)[source]

Return an integer sigma > sqrt(kappa + 2 n log2(2X)) * N^{5/2}.

This is the adaptive-security condition of ABMPSW20 Fig. 3 / Lemma 27 (the selective one drops the 2 n log2(2X) term).

Parameters:
  • n_mod (mpz) – RSA modulus N.

  • n_slots (int) – Total number of slots n.

  • bound_x (int) – Plaintext bound X.

  • kappa (int) – Statistical security parameter.

Return type:

mpz

class pyfe4ai.schemes.mcfe.paillier.MCFEPaillierKeyGenerator(config, **kwargs)[source]

Bases: IPFEAbsKeyGenerator, ParameterCacheMixin

Key generator for DCR-based multi-client inner-product FE (ABMPSW20 Fig. 3).

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the key generator.

Parameters:

config (dict) – Scheme configuration. Recognised keys: n (number of clients), lst_nid, eta (slots per client), bit_length (bits of each safe prime), bound_x, bound_y, stat_param (statistical parameter kappa), sec_param (recorded only).

Return type:

None

setup()[source]

Sample one Gaussian secret s_{i,j} per client slot.

Return type:

None

get_public_parameters()[source]

Return serialisable public parameters.

Return type:

dict

get_private_keys(nid, **kwargs)[source]

Return the encryption key {s} of client nid (None if unknown).

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

Derive the functional key k = sum y_{i,j} s_{i,j} (over Z).

Parameters:
  • sid (str) – Session identifier (unused).

  • **kwargs – Must include credentials with fusion_weight (dict mapping client IDs to integer weight lists).

Return type:

dict | None

Returns:

Dict with k, a single integer aggregated over all clients.

Raises:
  • FEKeyError – If credentials are missing.

  • FEValidationError – If a client id is unknown or a weight is malformed or exceeds bound_y.

class pyfe4ai.schemes.mcfe.paillier.MCFEPaillier(config, **kwargs)[source]

Bases: OfflineMCFEMixin, IPFEAbsCrypto

Crypto operations for DCR-based multi-client inner-product FE (ABMPSW20 Fig. 3).

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise with keys.pp and, for clients, keys.sk.

Parameters:

config (dict)

Return type:

None

encrypt(lst_pt, label)[source]

Encrypt one client’s integer vector under a label.

Parameters:
  • lst_pt (list) – Integer plaintext vector of length at most eta.

  • label (str) – Encryption label; a client must not encrypt twice under the same label.

Return type:

dict

Returns:

Dict with c (one element of Z_{N^2}^* per slot).

Raises:
  • FEKeyError – If public parameters or encryption keys are missing.

  • FEValidationError – If the plaintext is malformed or out of bound.

decrypt(dct_ct, dk, fusion_weight, label, term=None)[source]

Recover sum_i <x_i, y_i> from all clients’ ciphertexts under one label.

Parameters:
  • dct_ct (dict) – Mapping of client IDs to ciphertext dicts.

  • dk (dict) – Functional key {k} derived for fusion_weight.

  • fusion_weight (dict) – Mapping of client IDs to weight lists.

  • label (str) – The encryption label.

Returns:

The inner product (centred modulo N), or None if the result is not a valid decryption (e.g. wrong label or key).

Raises:
encrypt_lst_ndarray(lst_ndarray, **kwargs)[source]

Encrypt ndarrays element-wise (eta = 1), one derived label per element.

Parameters:
  • lst_ndarray (list) – List of numpy arrays.

  • **kwargs – Must include label.

Return type:

list | None

compute_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Aggregate-decrypt ndarray ciphertexts (delegates to decrypt_lst_ndarray_ct()).

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to ciphertext ndarray lists.

  • **kwargs – Must include dk, fusion_weight and label.

Return type:

list | None

decrypt_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Decrypt ndarray ciphertexts element-wise.

Parameters:
  • dict_ndarray_ct (dict) – Mapping of client IDs to ciphertext ndarray lists.

  • **kwargs – Must include dk, fusion_weight and label.

Return type:

list | None

Paillier Decentralized

Decentralised multi-client inner-product FE over DCR.

Abdalla, Bourse, Marival, Pointcheval, Soleimanian, Waldner:
“Multi-Client Inner-Product Functional Encryption in the Random-Oracle Model”
SCN 2020, https://eprint.iacr.org/2020/788 – Appendix D.1, Fig. 14
(DMCFE from MCFE) with MCFE1 = their DCR MCFE (Fig. 3,
pyfe4ai.schemes.mcfe.paillier) and MCFE2 = the same construction
over a larger modulus N2 (pyfe4ai.schemes.mcfe.decentralize) –
the instantiation Appendix D.1 discusses.
  • type: decentralised multi-client encryption (DMCFE, DCR, random oracle)

  • setting: Integer based (Z_{N^2}^* for data, Z_{N2^2}^* for key shares)

Encryption and decryption are those of the DCR MCFE. For weights y every client i publishes an MCFE2 encryption of its contribution k_i = sum_j y_{i,j} s_{i,j} (an integer: the MCFE1 key cannot be reduced modulo anything public) under the label H(y); the zero-sum MCFE2 keys make the product of all clients’ shares decrypt to k = sum_i k_i, the MCFE1 functional key. N2 is sized for |k_i| <= eta * Y * 12 sigma, with sigma the MCFE1 key width bounded through N < 2^(2 bit_length). Every client must publish a share. Secure against static corruptions (Theorem 38). Versions <= 0.2.0 used an ad-hoc label scalar, per-client key components and brute-forceable keys instead.

class pyfe4ai.schemes.mcfe.paillier_decentralized.DecentralizedMCFEPaillierKeyGenerator(config, **kwargs)[source]

Bases: MCFEPaillierKeyGenerator

Sets up DCR MCFE client keys and zero-sum MCFE2 keys (ABMPSW20 Fig. 14).

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the key generator.

Parameters:

config (dict) – As for MCFEPaillierKeyGenerator (n, lst_nid, eta, bit_length, bound_x, bound_y, stat_param).

Return type:

None

setup()[source]

Sample the DCR MCFE client keys and the zero-sum MCFE2 keys.

Return type:

None

get_public_parameters()[source]

DCR MCFE public parameters plus n2_mod, lst_nid and the bound.

Return type:

dict

get_private_keys(nid, **kwargs)[source]

Return client nid’s keys: DCR MCFE s and MCFE2 s2.

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

Not available: functional keys are derived by the clients.

Parameters:

sid (str)

Return type:

dict | None

class pyfe4ai.schemes.mcfe.paillier_decentralized.DecentralizedMCFEPaillier(config, **kwargs)[source]

Bases: MCFEPaillier

DCR MCFE encryption / decryption with decentralised key derivation.

Parameters:

config (dict)

derive_function_decryption_key_share(fusion_weight)[source]

Publish this client’s encrypted contribution sum_j y_{i,j} s_{i,j}.

Parameters:

fusion_weight (dict) – The whole weight mapping y (absent clients contribute zero).

Return type:

dict

Returns:

Dict with c (MCFE2 ciphertext), nid and weights_id.

combine_function_decryption_key_share(dct_dk_shares)[source]

Combine every client’s share into the DCR MCFE key {k}.

Raises:

FESchemeError – If a client’s share is missing or the shares were derived for different weights.

Parameters:

dct_dk_shares (dict)

Return type:

dict

FH-Multi-IPE (Pairing)

Function-hiding multi-client inner-product FE over pairings.

Agrawal, Goyal, Tomida, “Multi-Party Functional Encryption”, TCC 2021
(AGT21b), Section 6.2 – the FH-IP-MCFE as recapped in
Nguyen, Pointcheval, Schaedlich, “Decentralized Multi-Client Functional
Encryption with Strong Security”, IACR CiC 1(2), 2024, Section 2
(“Recap: The Function-Hiding MCFE of [AGT21b]”); selective security under
SXDH in the random-oracle model.
  • type: multi-client encryption with function-hiding keys (FH-MCFE)

  • setting: asymmetric pairing group (default BLS12-381), random oracle

Each client i holds the master key imsk_i of an independent function-hiding IPFE (pyfe4ai.schemes.mcfe.fh_ipe):

  • Enc: [omega]_1 = H1(tag); ct_i = iEnc(imsk_i, [x_hat_i]_1) with x_hat_i = (x_i, omega, 0, 0^eta, 0, 0).

  • KeyGen: fresh t_1, ..., t_n with sum t_i = 0; dk_i = iKeyGen(imsk_i, [y_hat_i]_2), y_hat_i = (y_i, t_i, 0, 0^eta, 0, 0).

  • Dec: prod_i iDec(dk_i, ct_i) = [sum <x_i, y_i> + omega sum t_i]_T = [<x, y>]_T, then a bounded discrete log.

The zero coordinates are the ones the security proof uses (NPS24 Section 2). Each client’s partial result is masked by omega t_i and the IPFE hides y_i; each client must encrypt at most once per tag. Symmetric pairing groups (SS512, …) are rejected because SXDH does not hold there. Versions <= 0.2.0 masked inputs with a public label scalar instead.

pyfe4ai.schemes.mcfe.fh_multi_ipe_pairing.vector_dim(eta)[source]

Length of the extended vectors (v, scalar, 0, 0^eta, 0, 0).

Parameters:

eta (int)

Return type:

int

class pyfe4ai.schemes.mcfe.fh_multi_ipe_pairing.FHContext(pp)[source]

Bases: object

Deserialised public parameters shared by the FH (D)MCFE schemes.

Parameters:

pp (dict)

zr(value)[source]
ser(element)[source]
Return type:

str

de(text)[source]
Parameters:

text (str)

de_many(texts)[source]
Parameters:

texts (list)

Return type:

list

h1(tag)[source]
Return type:

object

h2(tag_f)[source]
Parameters:

tag_f (str)

Return type:

object

x_hat(x, slot_scalar_elem)[source]

[(x, s, 0, 0^eta, 0, 0)]_1 with the scalar slot given as a G1 element.

Parameters:

x (list)

Return type:

list

y_hat(y, slot_scalar_elem)[source]

[(y, s, 0, 0^eta, 0, 0)]_2 with the scalar slot given as a G2 element.

Parameters:

y (list)

Return type:

list

static serialise_msk(msk, encrypt_only=False)[source]

Serialise an FH-IPFE master key; encrypt_only drops a_out, which only key generation needs.

Parameters:
  • msk (dict)

  • encrypt_only (bool, default: False)

Return type:

dict

static deserialise_msk(msk)[source]
Parameters:

msk (dict)

Return type:

dict

key_entry(key, z_sum)[source]
Parameters:
Return type:

dict

partial(ct, key_entry)[source]
Parameters:
solve(value)[source]
Return type:

int | None

class pyfe4ai.schemes.mcfe.fh_multi_ipe_pairing.MCFEFHMultiIPEPairingKeyGenerator(config, **kwargs)[source]

Bases: IPFEAbsKeyGenerator

Key generator for the AGT21b function-hiding MCFE.

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the key generator.

Parameters:

config (dict) – n, lst_nid, eta, bound_x, bound_y and pairing_group_param (asymmetric, default BLS12-381). The keys sec_level, u_bound and label_modulus of versions <= 0.2.0 are ignored.

Return type:

None

setup()[source]

Sample the generators and one FH-IPFE master key per client.

Return type:

None

get_public_parameters()[source]

Group description, generators, dimensions and bounds.

Return type:

dict

get_private_keys(nid, **kwargs)[source]

Return client nid’s encryption key {imsk} (None if unknown).

This is the slot’s FH-IPFE master key without the part only key generation needs. With it a client could strip the outer layer of its own slot of a functional key (outside the model: clients are not corrupted).

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

Derive {dk_i = iKeyGen(imsk_i, [y_hat_i]_2)} with fresh zero-sum t_i.

Parameters:
  • sid (str) – Session identifier (unused).

  • **kwargs – credentials with fusion_weight covering every client.

Return type:

dict | None

Returns:

Dict with dk (per client the FH-IPFE key k and [z]_T) and a random key_id (nothing derived from y: keys are function hiding).

class pyfe4ai.schemes.mcfe.fh_multi_ipe_pairing.MCFEFHMultiIPEPairing(config, **kwargs)[source]

Bases: IPFEAbsCrypto

Crypto operations for the AGT21b function-hiding MCFE.

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise with keys.pp and, for clients, keys.sk.

Parameters:

config (dict)

Return type:

None

encrypt(lst_pt, label)[source]

iEnc(imsk_i, [(x_i, H1(label), 0, ...)]_1); once per client and label.

Parameters:
Return type:

dict

decrypt(dct_ct, dk, fusion_weight=None, label=None)[source]

log prod_i iDec(dk_i, ct_i); None if out of bound (wrong tag or key).

fusion_weight and label are accepted for API compatibility and ignored: the key encodes y (function hiding) and the ciphertexts carry H1(label).

Raises:

FESchemeError – If the ciphertexts do not cover every client of the key.

Parameters:
encrypt_lst_ndarray(lst_ndarray, **kwargs)[source]

Encrypt ndarrays element-wise (eta = 1), one derived label per element.

Parameters:

lst_ndarray (list)

Return type:

list | None

compute_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Aggregate-decrypt ndarray ciphertexts (needs dk and label).

Parameters:

dict_ndarray_ct (dict)

Return type:

list | None

decrypt_lst_ndarray_ct(dict_ndarray_ct, **kwargs)[source]

Decrypt ndarray ciphertexts element-wise.

Parameters:

dict_ndarray_ct (dict)

Return type:

list | None

FH-Multi-IPE Decentralized (Pairing)

Decentralized function-hiding multi-client inner-product FE over pairings.

Nguyen, Pointcheval, Schaedlich, “Decentralized Multi-Client Functional
Encryption with Strong Security”, IACR CiC 1(2), 2024 – Figure 1, the
selectively function-hiding DMCFE (an extension of the AGT21b FH-MCFE of
under SXDH in the random-oracle model, static corruptions.
  • type: decentralised multi-client encryption with function-hiding keys

  • setting: asymmetric pairing group (default BLS12-381), random oracle

Setup samples one FH-IPFE master key imsk_i per client (pyfe4ai.schemes.mcfe.fh_ipe) and a sharing t~_1 + ... + t~_n = 0:

  • Enc: [omega]_1 = H1(tag); ct_i = iEnc(imsk_i, [x_hat_i]_1) with x_hat_i = (x_i, omega t~_i, 0, 0^eta, 0, 0).

  • DKeyGen: [mu]_2 = H2(tag-f); dk_i = iKeyGen(imsk_i, [y_hat_i]_2) with y_hat_i = (y_i, mu, 0, 0^eta, 0, 0).

  • Dec: prod_i iDec(dk_i, ct_i) = [<x, y> + omega mu sum t~_i]_T = [<x, y>]_T.

There is no authority for functional keys: each client derives its own dk_i from tag-f and its own weights y_i only, and keys of all clients for the same tag-f are needed; together they decrypt sum_i <x_i, y_i> for the weights each client chose. The proof allows one key per client and tag-f: name every new weight vector with a fresh tag_f. A client object refuses a second weight vector under a tag_f it already used (other instances cannot know about it), and derives the key randomness from a client secret, tag-f and y_i, so asking again returns the same key. Each client must encrypt at most once per tag. Earlier versions used tag-f = (tag_f, H(y)) over all clients’ weights, so a corrupted client could test guesses of the other clients’ weights; versions <= 0.2.0 masked inputs with a public label scalar instead.

class pyfe4ai.schemes.mcfe.fh_multi_ipe_pairing_decentralized.DecentralizedMCFEFHMultiIPEPairingKeyGenerator(config, **kwargs)[source]

Bases: MCFEFHMultiIPEPairingKeyGenerator

Setup of the NPS24 selectively function-hiding DMCFE.

Parameters:

config (dict)

setup()[source]

FH-MCFE setup plus the zero sharing t~ and per-client key seeds.

Return type:

None

get_private_keys(nid, **kwargs)[source]

Client nid’s imsk, its share t_tilde and its key-derivation seed.

Parameters:

nid (str)

Return type:

dict | None

get_decryption_keys(sid, **kwargs)[source]

Not available: functional keys are derived by the clients.

Parameters:

sid (str)

Return type:

dict | None

class pyfe4ai.schemes.mcfe.fh_multi_ipe_pairing_decentralized.DecentralizedMCFEFHMultiIPEPairing(config, **kwargs)[source]

Bases: MCFEFHMultiIPEPairing

Encryption, key derivation and decryption of the NPS24 FH-DMCFE.

Parameters:

config (dict)

encrypt(lst_pt, label)[source]

iEnc(imsk_i, [(x_i, H1(label)^t~_i, 0, ...)]_1); once per client and label.

Parameters:
Return type:

dict

derive_function_decryption_key_share(fusion_weight, tag_f)[source]

Derive this client’s key dk_i for its weights y_i (NPS24 DKeyGen).

Parameters:
  • fusion_weight – This client’s weights (eta integers), or a dict of weights in which only this client’s entry is used.

  • tag_f (str) – Non-empty name of the function; all clients use the same one for one functional key, and a fresh one for every new weight vector (one key per client and tag_f).

Return type:

dict

Returns:

Dict with k and z (the FH-IPFE key), nid and tag_f (nothing derived from y: keys are function hiding).

Raises:

FESchemeError – If this object already derived a key for other weights under tag_f.

combine_function_decryption_key_share(dct_dk_shares)[source]

Collect every client’s dk_i into the functional key.

Raises:

FESchemeError – If a client’s key is missing or the keys were derived for different function tags. (Keys for different weights under one tag_f combine into a key that decrypts to garbage.)

Parameters:

dct_dk_shares (dict)

Return type:

dict

FH-Multi-IPE Threshold (Pairing)

Threshold function-hiding multi-client inner-product FE over pairings.

The AGT21b FH-MCFE of pyfe4ai.schemes.mcfe.fh_multi_ipe_pairing with its functional key Shamir-shared among s decryption servers, any t of which decrypt together. Own construction (experimental, no published security proof):

  • the authority computes the FH-MCFE key for y in the exponent, dk_i = [e_i]_2 and z = sum_i z_i (with [z_i]_T the per-client correction), Shamir-shares every entry of every e_i and z modulo the group order and hands server k its shares in the exponent only: [e^(k)_{i,j}]_2 and [z^(k)]_T;

  • server k of an enrolled set S returns W_k = (prod_{i,j} e(ct_{i,j}, [e^(k)_{i,j}]_2) / [z^(k)]_T)^lambda_k with its Lagrange coefficient lambda_k;

  • prod_{k in S} W_k = prod_i iDec(dk_i, ct_i) = [<x, y>]_T, then a bounded discrete log.

Fewer than t servers learn nothing about the key (Shamir); t colluding servers interpolate [e_i]_2 and [z]_T in the exponent, the FH-MCFE functional key, which hides y. For any ciphertexts, the partial decryptions of an enrolled set are, in the exponent, evaluations of a random degree t - 1 polynomial whose constant term is the FH-MCFE decryption; a combiner can at most compute what the holder of the FH-MCFE key could, without learning the key itself. Earlier versions handed out the shares of e_i in Z_p, from which t colluding servers solved for y; versions <= 0.2.0 shared a public label-correction term instead.

class pyfe4ai.schemes.mcfe.fh_multi_ipe_pairing_threshold.ThresholdMCFEFHMultiIPEPairingKeyGenerator(config, **kwargs)[source]

Bases: MCFEFHMultiIPEPairingKeyGenerator

Key generator: FH-MCFE setup and Shamir-shared functional keys.

Parameters:

config (dict)

__init__(config, **kwargs)[source]

Initialise the key generator.

Parameters:

config (dict) – As for MCFEFHMultiIPEPairingKeyGenerator, plus s, t and lst_sid.

Return type:

None

setup()[source]

FH-MCFE setup and the client-server MAC keys; forgets previously shared keys.

Return type:

None

get_private_keys(nid, **kwargs)[source]

Client nid’s imsk plus mac_keys ({sid: key}) for the ciphertext tags.

Parameters:

nid (str)

Return type:

dict | None

get_public_parameters()[source]

FH-MCFE public parameters plus s, t and lst_sid.

Return type:

dict

get_decryption_keys(sid, **kwargs)[source]

Return server sid’s share of the FH-MCFE key for y.

All servers asking for the same weights receive shares of one key.

Parameters:
  • sid (str) – Decryption-server identifier, must be in lst_sid.

  • **kwargs – credentials with fusion_weight covering every client (a label entry is ignored: keys are label-free).

Return type:

dict | None

Returns:

Dict with k (per client the shares of the key exponents as G2 elements), z (the share of z as a GT element), sid, lst_sid, a random key_id (nothing derived from y) and mac_keys ({nid: key}), or None if sid is unknown.

class pyfe4ai.schemes.mcfe.fh_multi_ipe_pairing_threshold.ThresholdMCFEFHMultiIPEPairing(config, **kwargs)[source]

Bases: MCFEFHMultiIPEPairing

Crypto operations for the threshold FH-MCFE.

Parameters:

config (dict)

encrypt(lst_pt, label)[source]

FH-MCFE encryption under label; the ciphertext carries the encoded label and a tag for every server over both.

Parameters:
Return type:

dict

share_decrypt(dict_ct, credentials, dk, lst_sid_enrolled, label)[source]

Compute this server’s partial decryption W_k for an enrolled set.

Parameters:
  • dict_ct (dict) – One ciphertext per client (all under one label).

  • credentials (dict) – Ignored (the key share encodes y; servers need not know the function). Kept for API compatibility.

  • dk (dict) – This server’s key share from get_decryption_keys.

  • lst_sid_enrolled (list) – The servers whose partials will be combined.

  • label – The label (round) the server agrees to decrypt. The label is inside the FH ciphertexts, so the server checks the clients’ tags over the declared label and the ciphertext.

Return type:

dict

Returns:

Dict with w, ct_digest, key_id, label, sid and lst_sid_enrolled.

Raises:

FESchemeError – If a ciphertext is not under label or not tagged for this server by its client.

combine_decrypt(partials, label=None)[source]

Combine partial decryptions from at least t servers.

Parameters:
  • partials – {sid: share_decrypt(...)} for one enrolled set, or a list of such mappings for several enrolled sets, whose results must then agree (see combine_consistently(): servers are assumed honest-but-curious, and only this redundancy detects one that returns a wrong partial).

  • label (default: None) – Optional label the partials must be for.

Raises:

FESchemeError – If a group is incomplete or invalid, is for another label or its partial decryptions disagree on the ciphertexts they decrypted, or the groups disagree.

Return type:

int | None

compute_lst_ndarray_ct(dict_ndarray_ct, credentials, dk, lst_sid_enrolled, label)[source]

Partially decrypt ndarray ciphertexts element-wise; element (l, i) must be under its element label derived from label.

Parameters:
Return type:

list

decrypt_lst_ndarray_ct(dict_ndarray_ct, label)[source]

Combine element-wise partial decryptions from at least t servers.

Raises:

FESchemeError – If the servers’ outputs differ in structure, are for other labels, or an element does not decrypt.

Parameters:

label (str)

Return type:

list

Building Blocks

Label encoding shared by the labelled multi-client schemes.

MCFE ciphertexts are deterministic per (client slot, label), so a client must never encrypt twice under the same label. The ndarray helpers encrypt every array element under its own ElementLabel; encode_label() maps user labels (strings) and element labels into disjoint domains, so no user-chosen string can collide with the label of an array element.

class pyfe4ai.schemes.mcfe.labels.ElementLabel(label, layer, index)[source]

Bases: NamedTuple

Label of one ndarray element: the caller’s label, the layer and the index.

Parameters:
label: str

Alias for field number 0

layer: int

Alias for field number 1

index: tuple

Alias for field number 2

pyfe4ai.schemes.mcfe.labels.element_label(label, layer, index)[source]

Return the label of element index of layer layer under label.

Parameters:
Return type:

ElementLabel

pyfe4ai.schemes.mcfe.labels.encode_label(label)[source]

Return the unambiguous string that is hashed for label.

Parameters:

label – A user label (str) or an ElementLabel.

Raises:

FEValidationError – For any other type.

Return type:

str

pyfe4ai.schemes.mcfe.labels.chunk_label(label, chunk)[source]

Return the label of batch chunk chunk under label.

Used by schemes that pack many values into one ciphertext (one ciphertext per chunk and label); encoded in the element domain with layer -1, so it never collides with a user label or an element label.

Parameters:
Return type:

ElementLabel

Decentralised key derivation for MCFE schemes with a linear functional key.

Abdalla, Bourse, Marival, Pointcheval, Soleimanian, Waldner:
“Multi-Client Inner-Product Functional Encryption in the Random-Oracle Model”
SCN 2020, https://eprint.iacr.org/2020/788 – Appendix D.1, Fig. 14
(“DMCFE from MCFE”), secure against static corruptions (Theorem 38) if
MCFE1 is and MCFE2 is sel1-sta-pos+-IND secure.

Fig. 14 turns an MCFE (MCFE1) whose functional key is sk_y = sum_i y_i sk_{1,i} into a decentralised one:

  • KeyGen: each client has sk_{1,i} (MCFE1) and an MCFE2 key sk_{2,i} with sum_i sk_{2,i} = 0.

  • KeyDerShare: client i publishes Enc2(sk_{2,i}, y_i sk_{1,i}, H(y)) – an MCFE2 encryption of its key contribution under the label H(y).

  • KeyDerComb: Dec2 with the all-ones function, whose MCFE2 key is sum_i sk_{2,i} = 0, returns sum_i y_i sk_{1,i} = sk_y.

This module provides MCFE2: the DCR MCFE of ABMPSW20 Fig. 3 (see pyfe4ai.schemes.mcfe.paillier), one MCFE2 slot per coordinate of a key contribution, so the all-ones key of each coordinate is the sum of the clients’ MCFE2 keys for that coordinate. Its modulus N2 is sized for the contributions (Fig. 3 requires 2 n2 X2^2 < N2 for n2 slots of bound X2). Implementation choices: H(y) is a domain-separated hash of the canonical fusion weights; the zero-sum MCFE2 keys come from pairwise Gaussian masks r_{i,j} (client i adds r_{i,j} for j > i and subtracts it for j < i) – the “interactively produce” step of Fig. 14 – which this prototype samples in-process, as it does all other key material; a deployment would agree on each r_{i,j} pairwise.

pyfe4ai.schemes.mcfe.decentralize.MIN_PRIME_BITS = 128

Smallest prime size of N2, whatever the contribution bound.

pyfe4ai.schemes.mcfe.decentralize.key_label(fusion_weight)[source]

Return the MCFE2 label H(y) binding key shares to the weights y.

Parameters:

fusion_weight (dict)

Return type:

str

pyfe4ai.schemes.mcfe.decentralize.mcfe2_prime_bits(n_slots, bound, min_bits=128)[source]

Prime size for N2 = p q such that 2 n2 X2^2 < N2 (ABMPSW20 Fig. 3).

Parameters:
  • n_slots (int) – Number of MCFE2 slots n2 (clients x contribution size).

  • bound (int) – Bound X2 on the absolute value of a contribution entry.

  • min_bits (int, default: 128) – Lower limit on the prime size.

Return type:

int

pyfe4ai.schemes.mcfe.decentralize.generate_mcfe2_modulus(prime_bits)[source]

Return N2 = p q for fresh safe primes; the primes are discarded.

Parameters:

prime_bits (int)

Return type:

mpz

pyfe4ai.schemes.mcfe.decentralize.zero_sum_keys(lst_nid, dim, sigma)[source]

Return MCFE2 keys {nid: [s_{i,0..dim-1}]} with sum_i s_i = 0.

Built from pairwise masks r_{i,j} <- D_{Z^dim, sigma}, so each key is a sum of n - 1 Gaussians (width at least sigma) and the keys of all clients cancel coordinate-wise.

Parameters:
Return type:

dict

pyfe4ai.schemes.mcfe.decentralize.mcfe2_key_sigma(n2_mod, n_slots, bound, stat_param)[source]

Width of the pairwise masks: the ABMPSW20 Fig. 3 bound for MCFE2.

Parameters:
pyfe4ai.schemes.mcfe.decentralize.encrypt_contribution(n2_mod, mcfe2_key, contribution, label, bound)[source]

MCFE2 encryption of a key contribution, one Fig. 3 slot per coordinate.

Returns [(1 + N2)^{m_j} H(label)^{s_j} mod N2^2]_j as digit strings.

Parameters:
Return type:

list

pyfe4ai.schemes.mcfe.decentralize.combine_contributions(n2_mod, shares, lst_nid)[source]

MCFE2 decryption with the all-ones function (key 0) of every coordinate.

Parameters:
  • n2_mod – MCFE2 modulus N2.

  • shares (dict) – Mapping of every client ID to its encrypted contribution.

  • lst_nid (list) – All clients; the zero-sum keys only cancel if every client contributed.

Return type:

list

Returns:

[sum_i m_{i,j}]_j as signed integers (centred mod N2).

Raises:

FESchemeError – If a client is missing or a coordinate fails to decode.

Function-hiding single-input inner-product encryption over asymmetric pairings, with group-encoded inputs and output [<x, y>]_T.

Abdalla, Catalano, Fiore, Gay, Ursu, “Multi-Input Functional Encryption for
Inner Products: Function-Hiding Realizations and Constructions without
Pairings”, CRYPTO 2018 (ePrint 2017/972): Fig. 12 for a single slot
(n = 1), built from the AGRW17 scheme of Fig. 24 (inner layer) and the
ALS16 D_k-MDDH scheme of Fig. 7 over pairing groups (outer layer), with
k = 1 (SXDH); Appendix B (the LV16 padding x || 0, y || 0)
turns its weak function hiding into full function hiding (selective).

This is the black-box “FH-IPFE” that the function-hiding MCFE / DMCFE constructions of pyfe4ai.schemes.mcfe.fh_multi_ipe_pairing use: its encryption takes [x]_1 and its key generation [y]_2 (vectors of group elements), and decryption returns [<x, y>]_T = e(g1, g2)^{<x, y>}, so partial results of several instances can be multiplied.

With x' = (x, 0^m), y' = (y, 0^m) (m2 = 2m) and l = m2 + 3:

  • inner Enc: s <- Z_p; ct_in = [(A s ; (x', z) + U A s)]_1 in G1^l;

  • inner KeyGen: r <- Z_p; v = (y', r); sk_in = [(-U^T v ; v)]_2 in G2^l, z_sum = z r;

  • outer KeyGen of [ct_in]_1: ct = [(W'^T ct_in ; ct_in)]_1 in G1^{l+2};

  • outer Enc of [sk_in]_2: r' <- Z_p; key = [(-A' r' ; sk_in + W' A' r')]_2;

  • Dec: prod_j e(ct_j, key_j) / [z_sum]_T = [<ct_in, sk_in> - z r]_T = [<x, y>]_T.

A, A' in Z_p^2, U in Z_p^{(m2+1) x 2}, W' in Z_p^{l x 2} and z in Z_p form the master secret key.

class pyfe4ai.schemes.mcfe.fh_ipe.FHIPE(group, g1, g2, m)[source]

Bases: object

ACFGU18 Fig. 12 (n = 1, k = 1) with LV16 padding, for vectors of length m.

Parameters:
property gt

e(g1, g2), computed on first use (encryption does not need it).

identity1()[source]
identity2()[source]
setup()[source]

Sample the master secret key (all entries in Z_p).

Return type:

dict

encrypt(msk, x_enc, rng=None)[source]

Encrypt [x]_1 (m elements of G1); returns l + 2 elements of G1.

rng (an object with getrandbits) makes the randomness deterministic.

Parameters:
Return type:

list

keygen(msk, y_enc, rng=None, r=None)[source]

Key for [y]_2 (m elements of G2); returns (l + 2 elements of G2, z_sum).

rng (an object with getrandbits) makes the randomness deterministic; r fixes the inner-layer randomness (shared by all slots of an ACFGU18 Fig. 12 key, whose pad term is then sum_i z_i r).

Parameters:
Return type:

tuple

keygen_exponents(msk, y, r=None)[source]

Like keygen() for a vector known in the clear, returning exponents.

Returns (e, z_sum) with the key equal to [e]_2; used to secret-share a key (threshold decryption).

Parameters:
Return type:

tuple

decrypt(ct, key, z_sum_gt)[source]

Return [<x, y>]_T from a ciphertext, a key and the key’s [z_sum]_T.

Parameters: