v0.4.0 Functional encryption for trustworthy AI

Compute on encrypted data. Reveal only the result.

PyFE4AI is a research-oriented Python library of functional encryption (FE) schemes for AI security and privacy — from single-input inner products to multi-client, threshold, decentralized and quadratic constructions, with an optional Rust core for the hot paths.

Functional encryption in one picture Three clients hold vectors x1 = (2, 1, 3), x2 = (1, 0, 2) and x3 = (3, 2, 1) and send only ciphertexts. A key holder with the functional key for the weights y1 = (4, 5, 6), y2 = (1, 1, 1) and y3 = (2, 0, 1) decrypts a single number, the sum of the inner products, 41, and nothing else about the vectors. 1 · ENCRYPT 2 · DECRYPT WITH dk_y 3 · REVEAL CLIENT 1 · x₁ 213 9f3ac7 CLIENT 2 · x₂ 102 e1047b CLIENT 3 · x₃ 321 5db2f8 dk_y key holder key for y₁, y₂, y₃ Enc(x₁) Enc(x₂) Enc(x₃) Σ⟨xᵢ, yᵢ⟩ ? 41 only the result, in the clear
Schemes
42registered schemes
Assumptions
5hardness assumptions
Native backend
Rustoptional native core
Tests
~4,100tests, both backends
License
Apache-2.0open source
Why PyFE4AI

The functional-encryption zoo, behind one entry point

pyfe4ai.create("family/variant", config) returns a scheme's key generator and crypto class; then setup / get_decryption_keys / encrypt / decrypt — across every family, built for ML workloads.

Complete FE family coverage

SIFE, MIFE, MCFE, decentralized MCFE, threshold MIFE/MCFE, function-hiding IPE / Multi-IPE, and quadratic FE (SGP, Quad, multi-input MIQFE) — 42 (family, variant) combinations under one roof.

Five hardness assumptions, six instantiations

DDH (3072-bit groups or the ristretto255 curve) and its matrix form MDDH (Damgård), Paillier/DCR, LWE and Ring-LWE (lattice lines), and pairings (BLS12-381) — pick the assumption that fits your deployment.

ML-native adapter

pyfe4ai.ml runs SIFE and MCFE schemes for encrypted linear inference and gradient aggregation, including threshold MCFE over Ring-LWE with its decryption servers in-process. Its wrapper holds the master key and plays every party (one trust domain, for experiments) and by default refuses MIFE aggregation, partial aggregates and label reuse; the other threshold and decentralized variants go through the scheme classes. A quantization toolkit handles float ↔ integer precision.

Engineered for speed

An optional Rust extension, built by pip install when cargo is available, runs the lattice hot paths, ristretto255 and BLS12-381 pairings with the outputs of the Python reference: an MCFE Ring-LWE round of 10 000 gradient elements under standard takes about 18 ms per client and 7.5 ms to aggregate, against 345 / 157 ms on the Python backend (JSON ciphertexts, one thread).

Tested

About 4,100 tests, run in CI with and without the Rust extension (the native code is checked bit for bit against the Python reference), including status-table sync, security-profile and crypto-review regression tests, plus a structured FEError exception hierarchy.

Paper-referenced

Every scheme cites its source construction (PKC'15, CRYPTO'16/'18, ASIACRYPT'18, SCN'20, TCC'21/'22, PKC'22, IACR CiC'24, IEEE TDSC TAPFed, …) in the module docstring. Deviations from the paper are listed in the Scheme Status table.

Where FE fits

Who sees what — FE next to other privacy technologies

Functional encryption gives the holder of a functional key exactly f(x), in the clear, and nothing else about x. Other techniques draw the line elsewhere. A simplified, qualitative picture of typical deployments:

Functional encryption this library
Input · x stays with its owner
Computing · the decryptor sees only Enc(x)
Result · the key holder learns f(x), nothing else
Homomorphic encryption HE / FHE
Input · x stays with its owner
Computing · the evaluator computes on Enc(x)
Result · Enc(f(x)); the secret-key holder decrypts
Secure multi-party computation MPC
Input · x stays with its owner
Computing · parties exchange shares or masked messages
Result · designated parties learn f(x)
Differential privacy DP, central model
Input · x is sent to a curator
Computing · the curator sees x in the clear
Result · f(x) + calibrated noise, for anyone
Trusted execution TEE / enclave
Input · x leaves its owner encrypted, for the enclave
Computing · clear inside the enclave, hidden from the host
Result · whatever the enclave code releases
Qualitative comparison of functional encryption with homomorphic encryption, secure multi-party computation, differential privacy and trusted execution environments
TechniqueFunctionsInteractionMust be trustedOutputPost-quantum options
FE Specific classes: here inner products and quadratic functions None to compute: one ciphertext per client, then decryption with a functional key from the authority; threshold variants add one partial decryption per server The key authority (setup and functional keys); the hardness assumption f(x) in the clear, to the key holder Yes: the LWE and Ring-LWE schemes (dMCFE-LWE derives its keys under DCR); not the DDH, DCR or pairing ones
HE Arbitrary circuits with fully homomorphic schemes; narrower classes with additive ones None while computing; the key holder decrypts the result The secret-key holder (or a threshold of them); the hardness assumption Encrypted until the key holder decrypts Yes: the common FHE schemes are lattice-based
MPC Arbitrary functions Parties exchange messages during the computation That no more than a threshold of parties collude, in the protocol's adversary model In the clear, to the designated parties Yes: information-theoretic protocols exist; others depend on their primitives
DP Statistics and learning that tolerate noise None The curator who sees the raw data (central model; local DP removes it at a cost in accuracy) In the clear, with calibrated noise Not applicable: a statistical guarantee, not a hardness assumption
TEE Arbitrary code Remote attestation, then a secure channel to the enclave The hardware vendor, attestation, and the absence of side channels As the enclave code releases it Rests on hardware isolation, not on a hardness assumption

Simplified on purpose: each row stands for a family of techniques with many variants (threshold and multi-key HE, MPC with a dealer, local and central DP, …), and they combine — noise can be added before FE encryption, or MPC can replace a trusted setup. No performance comparison is implied.

FE's limits, honestly

  • Narrow function classes: inner products and quadratic functions, not arbitrary programs.
  • The result itself is revealed: every functional key discloses one value f(x), and enough keys (e.g. η independent weight vectors for vectors of length η) determine x — which keys exist is the key authority's decision.
  • Trusted setup: most schemes need a key authority for setup and functional keys; the decentralized variants drop it for functional keys but still have a trusted setup in this library, and the threshold variants assume honest-but-curious servers (Choosing a scheme, Security & status).
How it works

One federated-learning round, three ways to deploy it

Clients encrypt their gradients under the round's label; whoever holds the functional key for the weights learns the weighted sum — not a single client's gradient. Step through it, or let it play.

Messages of one aggregation round between key authority, clients, servers and aggregator; the steps are listed next to the diagram Key authority setup() · master key Trusted setup once, in process Client 1 gradient x₁ Client 2 gradient x₂ Client 3 gradient x₃ Server 1 key share 1 Server 2 key share 2 Server 3 key share 3 Aggregator functional key dk_y Aggregator combines key shares Combiner holds no secret Σ yᵢ·xᵢ in the clear ℓ = the round's label
  1. Once: the key authority runs setup() and gives every client its encryption key skᵢ.
  2. Each round has a label ℓ. Every client encrypts its gradient under ℓ and sends only the ciphertext — one per client and label.
  3. The authority issues the aggregator a functional key for the weights y (in mcfe/ring_lwe also bound to ℓ).
  4. The aggregator decrypts Σ yᵢ·xᵢ for label ℓ — the weighted sum, never a single gradient. Ciphertexts of different labels do not combine.
  1. The key authority gives every client its encryption key and each of the s = 3 servers its share of the functional key for y; with t < s the LWE layers use a replicated sharing (the default).
  2. Clients encrypt as usual under ℓ; the ciphertexts reach the servers (in tMCFE-Ring-LWE tagged for each server).
  3. An enrolled set of t = 2 servers computes partial decryptions with share_decrypt; the LWE layers flood them with noise. Server 3 can stay offline.
  4. A combiner — any party, it holds no secret — joins the t partials with combine_decrypt and decodes Σ yᵢ·xᵢ. Fewer than t servers learn nothing beyond the result.
  1. No authority for functional keys — but the setup is trusted: in this library one key generator samples every client's keys, including the pairwise zero-sum secrets, in process.
  2. Each round, every client sends its ciphertext; for each functional key it also sends its own share of the key for y (every round in dMCFE-Ring-LWE, whose keys are bound to the label).
  3. The aggregator combines the shares into the functional key and decrypts Σ yᵢ·xᵢ.

Threshold servers are assumed honest-but-curious: a partial decryption carries no proof of correctness. pyfe4ai.ml runs MCFE — and the servers of mcfe/ring_lwe_threshold — in one process for experiments. Details: Threshold decryption and the ML adapter.

Inside the library: one call through the stack

Python on top, one backend interface in the middle, and two implementations of it: the Python reference and the optional Rust extension, which computes the same values. Follow a call down to the Rust core and back.

The layers of PyFE4AI from your code to the Rust core: application, ML adapter, pyfe4ai.create and the registry, schemes, primitives, the backend interface, and its two implementations, the Rust extension with rust/core and the Python reference; the steps are listed next to the diagram Application your code · examples/ · benchmarks/ you ML adapter FESchemeWrapper · encrypt_gradient · aggregate_gradients · quantization pyfe4ai.ml pyfe4ai.create · registry config · security profiles · review status · formats · versions pyfe4ai.core Schemes · 42 sife · mife · mcfe · quadratic · threshold · common pyfe4ai.schemes Primitives lattice · sampling · hashing · groups · pairing pyfe4ai.primitives Backend interface ops() · ring(q, n) · PYFE4AI_BACKEND = auto | python | native pyfe4ai.backend Rust · pyfe4ai._rust_native rust/native (PyO3) · GIL released BLS12-381 · ristretto255 · ChaCha20 per call rust/core · no dependencies · C ABI for tests ntt · rlwe · rns · facct · keccak · shamir · … Python reference python_impl gmpy2 · NumPy
  1. pyfe4ai.create("mcfe/ring_lwe", cfg) — the registry finds the scheme by id and builds its key generator, the config completed by the security profile; you get kg and the class MCFERingLWE.
  2. client.encrypt_lst_ndarray([grad], label=ℓ) — the scheme packs the tensor into chunks: one Ring-LWE ciphertext per chunk, each with its own label pad.
  3. Primitives: the label is hashed to Zq, and the noise comes from the scheme's sampler — exact (FACCT) Gaussians by default for the Ring-LWE family.
  4. backend.ring(q, n).encrypt_tensor(…) — under auto the extension is built and q < 2253 has the root of unity it needs, so the native implementation runs.
  5. pyfe4ai._rust_native runs the rust/core kernels: one call per tensor with the GIL released, chunks in parallel with PYFE4AI_THREADS, one ChaCha20 stream per chunk from one OS seed, overwritten when the call ends.
  6. The ciphertexts return to your code — JSON by default for mcfe/ring_lwe, or binary with ct_format="binary".
  1. encrypt_gradient(grad, wrapper, nid="c1", label=ℓ) — one client's step of a federated round.
  2. pyfe4ai.ml reserves (client, ℓ) — a second encryption under ℓ is refused — and takes the client's crypto object, which the wrapper built from the registry.
  3. The scheme scales the floats to integers at the wrapper's precision and packs them into chunks …
  4. … through the same primitives, backend interface and Rust kernels as a direct call.
  5. An EncryptedGradient comes back, ready to serialise for the aggregator (to_dict()).
  1. PYFE4AI_BACKEND=python — or the extension is not built, or the parameters are outside what it handles (moduli of 2253 or more, rings without a 2n-th root of unity, wider Gaussians).
  2. The layers above the backend run the same code as before.
  3. The backend interface hands the operation to the Python reference (backend/python_impl: gmpy2, NumPy).
  4. Same results, only slower: the tests run every backend operation through both implementations and require the same outputs. Without the extension the library says so once per process (FEWarning; not under an explicit python).

Simplified: the lanes on the left carry the call down (amber) and the result back up (teal); the layers above the backend also share pyfe4ai.core. What runs natively is listed in the docstring of pyfe4ai.backend and in Threads, processes and copies.

Python is the reference

The extension computes the reference's values — same NTT roots and tables, samplers and hash reductions — and tests compare the two.

One random source per call

A tensor call expands one 32-byte OS seed with ChaCha20, one stream per chunk; seed and states are overwritten when it ends.

GIL released, threads optional

One native call per tensor; PYFE4AI_THREADS=k lets it use k threads (default 1).

Unsupported inputs fall back

Inputs it does not handle go to the reference: moduli ≥ 2253, rings without the root of unity, wider Gaussians.

Scheme families

42 schemes, one registry

A scheme is named "family/variant": the family says who encrypts and who decrypts, the variant which assumption and construction it uses. pyfe4ai.list_schemes() and pyfe4ai.get_scheme_status("family/variant") answer the same questions at runtime.

SIFE

11

single-input

One encryptor; the holder of a key for y learns ⟨x, y⟩ — one party's vector scored by another party's linear model.

MIFE

11

multi-input

n clients each encrypt a slot; the key holder learns Σ⟨xᵢ, yᵢ⟩. No labels: for one-shot joint inputs, not for repeated rounds.

MCFE

17

multi-client, labelled

Clients encrypt under a shared label, and only ciphertexts of one label combine — federated aggregation, one label per round.

Quadratic

3

SGP · Quad · MIQFE

The key holder learns xᵀFy (sgp, quad), or the cross-user quadratic terms of several clients (miqfe).

  • 6 threshold — the functional key shared among s servers, any t decrypt
  • 5 decentralized — no authority for functional keys (trusted setup)
  • 7 function-hiding — keys also hide the weights
  • 4 refused under standard

Families × instantiations

  • reviewed
  • experimental
  • unreviewed
  • known-insecure (none)
Registered schemes by family (rows) and instantiation (columns), with each scheme's review status
FamilyDDHDamgårdPaillierLWERing-LWEPairing
SIFE single-input
MIFE multi-input
MCFE multi-client, labelled
dMCFE decentralizednone
tMIFE thresholdnonenonenonenone
tMCFE thresholdnonenone
Quadratic SGP · Quad · MIQFEnonenonenonenonenone

Select a cell to list its schemes, their review status and whether standard refuses them.

One dot per registered scheme, from the registry (pyfe4ai/core/registry.py). The DDH column includes the ddh_ristretto255 schemes; pairing-based and quadratic schemes run on BLS12-381 from the Rust extension, other curves need charm-crypto. Per-scheme papers, fidelity and deviations: Security & status and Choosing a scheme.

Use cases

Built for privacy-preserving ML

Functional encryption reveals a chosen function of the data — not the data itself.

Secure aggregation in federated learning

Clients encrypt local gradients with MCFE-DDH (CDGPP18), the DCR, LWE and MDDH constructions of ABMPSW20, or MCFE-Ring-LWE (BMMS21), which under standard encrypts 10 000 gradient elements in about 18 ms per client with the Rust extension (0.35 s in pure Python); the aggregator's functional key decrypts only the weighted sum. dMCFE variants need no authority for functional keys (their setup, which samples every client's keys in-process, is still trusted).

Threshold aggregation

tMCFE variants (TAPFed-style) split decryption across t-of-s servers that are assumed honest-but-curious; their LWE, Ring-LWE and pairing threshold layers are experimental. For mcfe/ring_lwe_threshold the ML adapter runs the servers in-process — for experiments — and shows the messages of a deployment (partial_decrypt_gradients, combine_partial_decryptions).

Performance

A Rust core under the Python reference

The same 0.4.0 code on its two backends — the Rust extension and the plain Python implementation — on one machine, one thread, under standard. Ciphertext formats are labelled: JSON is the default of mcfe/ring_lwe, binary that of the threshold MCFE.

18.2 msper client to encryptMCFE Ring-LWE, 104 elements · JSON (default) · Python backend: 345 ms
7.5 msto aggregateJSON (default) · Python backend: 157 ms
0.25 msper client onlineafter an offline phase · binary ciphertexts
24.6 Bper elementbinary ciphertexts · 68.4 B as JSON
MCFE Ring-LWE · mcfe/ring_lwe, 104 elements by ciphertext format
client · JSON (default)345 ms18.2 ms
aggregator · JSON (default)157 ms7.5 ms
client · binary360 ms17.95 ms
aggregator · binary164 ms6.24 ms
Threshold MCFE · 104 elements, s = 3, t = 2 defaults: binary ciphertexts, RNS modulus, split key shares; aggregation = every party of the round on one machine
client493 ms26.4 ms
partial decryption, per server274 ms8.0 ms
aggregation: key shares, t partial decryptions, combine686 ms27.8 ms
Dense-LWE MCFE · mcfe/lwe, 200 elements JSON ciphertexts (default)
client543 ms92.6 ms
aggregator551 ms100.1 ms
Rust backend only no Python comparison: the BLS12-381 pairing group has no Python implementation (both backends use the Rust one), and the Python backend of ristretto255 was not measured
Pairing schemes on BLS12-381, one round of setup, keys, encryption and decryption
SGP 30 ms · Quad 48 ms · MCFE FH-Multi-IPE 47 ms
DDH MCFE over ristretto255 (ddh_ristretto255, unreviewed), per element
client 0.069 ms · aggregator 0.139 ms · ciphertext 75 B
Show these numbers as a table
Timings of the Rust and Python backends, from benchmarks/PERFORMANCE.md
WorkloadMeasurePython backendRust backend
MCFE Ring-LWE, 104 elementsclient, JSON (default)345 ms18.2 ms
aggregator, JSON (default)157 ms7.5 ms
client, binary360 ms17.95 ms
aggregator, binary164 ms6.24 ms
online, after the offline phase (binary)—0.25 ms client · 3.16 ms aggregator
4 threads (binary)—6.21 ms client · 3.62 ms aggregator
Threshold MCFE, 104 elements, s = 3, t = 2, defaults (binary)client493 ms26.4 ms
partial decryption, per server274 ms8.0 ms
aggregation (key shares, t partial decryptions, combine)686 ms27.8 ms
Dense-LWE MCFE, 200 elements (JSON)client543 ms92.6 ms
aggregator551 ms100.1 ms
Pairing round, BLS12-381SGPno Python implementation30 ms
Quad48 ms
MCFE FH-Multi-IPE47 ms
DDH MCFE over ristretto255, per elementclientnot measured0.069 ms
aggregator0.139 ms
ciphertext75 B

Apple M2 Max, Python 3.13, one thread unless stated, security_profile="standard", three clients, medians of interleaved runs; timings vary with the machine and its load. Bars are to scale within each group. Source and commands: benchmarks/PERFORMANCE.md.

Quickstart

From clone to first inner product

Encrypt a vector, hand out a functional key, learn only the inner product. Demo parameters are toy-sized: sec_param is a modulus bit length, not a security level; "security_profile": "standard" selects 128-bit-oriented parameters, except for pairings (BLS12-381, about 120–126-bit) and the schemes it refuses (Security Profiles).

install · shell
# from source
git clone https://github.com/spire-studio/pyfe4ai.git
cd pyfe4ai
conda env create -f environment.yml
conda activate pyfe4ai
# with cargo on the PATH this also builds the optional Rust
# extension (BLS12-381 for the pairing & quadratic FE)
pip install -e ".[test]"

# other pairing curves (charm-crypto): GMP, OpenSSL, PBC first
pip install -e ".[test,pairing]"

# run the core test suite
python -m pytest -m core -q

sife_ddh.py

import pyfe4ai

x, y = [2, 1, 3], [4, 5, 6]

# sec_param = 128 is a toy modulus size: fast, NOT secure. For a
# 3072-bit group pass {"security_profile": "standard", "eta": 3}.
kg, SIFEDDH = pyfe4ai.create("sife/ddh", {"sec_param": 128, "eta": 3})
kg.setup()
pp, sk = kg.get_public_parameters(), kg.get_private_keys()
dk = kg.get_decryption_keys("sid_0",
        credentials={"fusion_weight": y})

ct = SIFEDDH({"precision": 3, "keys": {"pp": pp, "sk": sk}}).encrypt(x)
result = SIFEDDH({"precision": 3, "keys": {"pp": pp}}).decrypt(ct, dk, y)
print(result)  # 31 = ⟨x, y⟩

mcfe_ddh.py

import pyfe4ai

x = {"alice": [1, 2], "bob": [3, -1], "carol": [0, 4]}
y = {"alice": [2, 1], "bob": [1, 1], "carol": [-1, 3]}

kg, MCFEDDH = pyfe4ai.create("mcfe/ddh", {"eta": 2, "n": 3, "lst_nid": list(x)})
kg.setup()
pp = kg.get_public_parameters()

# every client encrypts its own vector under the round's label
label = "round-1"
cts = {}
for nid in x:
    client = MCFEDDH({"id": nid, "keys": {"pp": pp, "sk": kg.get_private_keys(nid)}})
    cts[nid] = client.encrypt(x[nid], label)

# the functional key reveals only the weighted sum, for one label
dk = kg.get_decryption_keys("aggregator", credentials={"fusion_weight": y})
aggregator = MCFEDDH({"id": "aggregator", "keys": {"pp": pp}})
print(aggregator.decrypt(cts, dk, y, label))  # 18

fl_round.py

import numpy as np
from pyfe4ai.ml import FESchemeWrapper, aggregate_gradients, encrypt_gradient

clients = ["c0", "c1", "c2"]
# the wrapper runs the key generator and plays every party: experiments only
wrapper = FESchemeWrapper("mcfe", "ring_lwe",
                          {"eta": 1, "n": 3, "lst_nid": clients,
                           "bound_x": 10**4, "bound_y": 10},
                          precision=3)
grads = {nid: np.random.default_rng(i).uniform(-1, 1, 1000)
         for i, nid in enumerate(clients)}
enc = {nid: encrypt_gradient(g, wrapper, nid=nid, label="round-1")
       for nid, g in grads.items()}
total = aggregate_gradients(enc, wrapper, {nid: 1 for nid in clients},
                            label="round-1")
print(np.allclose(total, sum(grads.values()), atol=1e-2))  # True

registry.py

import pyfe4ai

print(len(pyfe4ai.list_schemes()))                         # 42
print(pyfe4ai.list_schemes("mcfe", capabilities="threshold"))  # the threshold MCFEs
scheme = pyfe4ai.get_scheme("mcfe/ring_lwe")
print(scheme.kind, sorted(scheme.capabilities), scheme.status.status)
print(pyfe4ai.get_scheme_status("mcfe/ring_lwe_threshold").status)  # experimental
Security & status

Honest about what is reviewed — and what is not

Every registered scheme carries a review status in the registry, reported at runtime by pyfe4ai.get_scheme_status("family/variant").

Review status

reviewed
34 checked against the cited construction; no defect known
experimental
5 our own layer, no published proof: the threshold layers of tMIFE-LWE, tMCFE-LWE, tMCFE-Ring-LWE and tMCFE-FH-Multi-IPE, and the dMCFE-DDH carry mask
unreviewed
3 the DDH schemes over ristretto255, new code
known-insecure
0 none is known to be insecure

Security profiles

"toy" default

Fast and not secure: sec_param is a modulus bit length, LWE dimensions default to 32–64 and the Ring-LWE dimension to 32.

"standard"

Aims at 128-bit security: 3072-bit discrete-log groups (RFC 3526) and DCR moduli, lattice-estimator-sized LWE and Ring-LWE dimensions, stat_param 128. Pairings run on BLS12-381, about 120–126-bit (a FESecurityWarning says so).

Refused under standard — no parameter set both secure and usable: sife/fullysec_lwe mife/fullysec_lwe mife/lwe_threshold mcfe/paillier_decentralized

Trust model

  • Threshold servers are honest-but-curious: partial decryptions carry no proof of correctness; redundant enrolled sets can detect a deviating server.
  • Decentralized MCFE needs no authority for functional keys, but its setup is trusted.
  • The ML adapter holds the master key and plays every party: one trust domain, for experiments.
  • No side-channel hardening; authenticated channels between parties are assumed.
Cite

Citing PyFE4AI

If you use PyFE4AI in your research, please cite the software. A technical report is in preparation coming soon

pyfe4ai.bib
@software{pyfe4ai2026,
  author  = {{Spire Studio}},
  title   = {{PyFE4AI}: Python-based Functional Encryption
             for {AI} Security and Privacy},
  year    = {2026},
  version = {0.4.0},
  license = {Apache-2.0},
  url     = {https://github.com/spire-studio/pyfe4ai}
}

Citation metadata is also in CITATION.cff — GitHub's “Cite this repository” button gives APA and BibTeX.