Skip to content

Limits and refusals

Everything the library refuses, and why a refusal was chosen over a plausible number.

The general rule

A silent zero is worse than an exception. A NaN turned into a credible zero and carried into a sum is indistinguishable from a real zero — and a gap in a feature column is ordinary input, not an exotic case.

The key

condition what happens
modulus shorter than 2048 bits refused: below NIST SP 800-57 for 112 bits
modulus longer than 8192 bits refused: denial-of-service guard
(p−1)/2 or (q−1)/2 not prime refused: safe primes are required
\|p − q\| small refused: factors by Fermat's method
p = q refused: no CRT split exists

The upper bound is not about security, it is about denial of service. Building a peer key runs with the GIL released, and until it returns the interpreter executes nothing at all, signal handlers included.

A foreign modulus is cut off by raw byte count, before any arithmetic. Previously was computed before the check: 64 megabytes from a peer bought 4.07 seconds of complete deafness. Now the refusal does not look at the content at all, and its cost does not depend on the input length — which is asserted by a test rather than promised.

The plaintext

condition what happens
NaN, ±inf refused
overflow when scaled refused
magnitude above the key bound refused

The bound on a value is n/2 shrunk by 2^20 to reserve headroom for a sum. With a key of 2048 bits or more that is 2^2026, while the largest encodable f64 is about 2^1024 — so in practice this check cannot fire. It stands anyway: key length and scale are configurable, and a defence against one combination is not a defence against another.

The sum

condition what happens
empty batch refused
more than 2^20 terms per call refused
ciphertext outside [1, n²) refused, with the term's index
mixed scales in a batch refused, naming both scales

The cap on terms is not a guarantee

It is per call. The result of add_many can be fed into a second call and the counter starts over; two lawful calls give 2^40 terms, and no check will see it — the ciphertext of a sum is indistinguishable from the ciphertext of a term.

What really keeps sums in the group is the thousand bits of headroom between the key bound and what is encodable from f64.

What is NOT checked

That a ciphertext was made under this key. It does not follow from n alone. A foreign ciphertext will usually lead to a refusal on decryption, but not always.

That a ciphertext is invertible. add_many checks only the range: the value n falls inside [1, n²), yet gcd(n, n²) ≠ 1, and a sum containing it is spoiled. The refusal then arrives later, at the key holder, with no address on it. This is deliberate: a gcd per term would cost more than the operation itself.

That the modulus is not poisoned. Length and oddness are checked — partial public key validation per NIST SP 800-56B, which is what everyone does. A smoothness probe gives no guarantee at any bound: at B = 2^20 it costs 25 minutes and certifies 2^10 of work.

That the scale byte is authentic. It is not authenticated, and flipping one byte yields a plausible wrong number. This adds nothing for an active attacker — Paillier is malleable anyway — but the scale mechanism defends against oversight, not against edits on the wire.