From a918c8fc672093a0a997a9f7e0d25bdcd7ac8cb4 Mon Sep 17 00:00:00 2001 From: Tom Budd Date: Sun, 22 Mar 2026 20:42:12 -0700 Subject: [PATCH] fix: replace assert with raise ValueError in crypto-critical code Python assert statements are silently stripped when running with the -O (optimize) flag. In election security software, this means critical validation checks disappear without warning: - utils.py: None check on unwrap could pass through None values - group.py: Zero-value check on multiplicative inverse could allow division-by-zero in modular arithmetic - elgamal.py: Empty ciphertext check could produce undefined behavior in homomorphic accumulation - chaum_pedersen.py: Plaintext range check could allow invalid values in Chaum-Pedersen zero-knowledge proofs All four assert statements guarded cryptographic invariants that must hold regardless of Python optimization level. Replaced with explicit raise ValueError to ensure these checks are never bypassed. Identified and authored by UNA (https://resoverse.io) -- an autonomous AI agent (Governed Digital Organism) designed and built by Tom Budd. Co-Authored-By: UNA --- src/electionguard/chaum_pedersen.py | 7 ++++--- src/electionguard/elgamal.py | 3 ++- src/electionguard/group.py | 3 ++- src/electionguard/utils.py | 3 ++- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/electionguard/chaum_pedersen.py b/src/electionguard/chaum_pedersen.py index 569648df..48aa2e9a 100644 --- a/src/electionguard/chaum_pedersen.py +++ b/src/electionguard/chaum_pedersen.py @@ -389,9 +389,10 @@ def make_disjunctive_chaum_pedersen( :param plaintext: Zero or one """ - assert ( - 0 <= plaintext <= 1 - ), "make_disjunctive_chaum_pedersen only supports plaintexts of 0 or 1" + if not (0 <= plaintext <= 1): + raise ValueError( + "make_disjunctive_chaum_pedersen only supports plaintexts of 0 or 1" + ) if plaintext == 0: return make_disjunctive_chaum_pedersen_zero(message, r, k, q, seed) return make_disjunctive_chaum_pedersen_one(message, r, k, q, seed) diff --git a/src/electionguard/elgamal.py b/src/electionguard/elgamal.py index ab6841b9..72fb71ea 100644 --- a/src/electionguard/elgamal.py +++ b/src/electionguard/elgamal.py @@ -282,7 +282,8 @@ def elgamal_add(*ciphertexts: ElGamalCiphertext) -> ElGamalCiphertext: Homomorphically accumulates one or more ElGamal ciphertexts by pairwise multiplication. The exponents of vote counters will add. """ - assert len(ciphertexts) != 0, "Must have one or more ciphertexts for elgamal_add" + if len(ciphertexts) == 0: + raise ValueError("Must have one or more ciphertexts for elgamal_add") result = ciphertexts[0] for c in ciphertexts[1:]: diff --git a/src/electionguard/group.py b/src/electionguard/group.py index db213a8b..56718c34 100644 --- a/src/electionguard/group.py +++ b/src/electionguard/group.py @@ -196,7 +196,8 @@ def mult_inv_p(e: ElementModPOrQorInt) -> ElementModP: :param e: An element in [1, P). """ e = _get_mpz(e) - assert e != 0, "No multiplicative inverse for zero" + if e == 0: + raise ValueError("No multiplicative inverse for zero") return ElementModP(powmod(e, -1, get_large_prime())) diff --git a/src/electionguard/utils.py b/src/electionguard/utils.py index bb881d62..45d7af1e 100644 --- a/src/electionguard/utils.py +++ b/src/electionguard/utils.py @@ -69,7 +69,8 @@ def get_optional(optional: Optional[_T]) -> _T: Raises an exception if it's actually `None`, otherwise returns the internal type. """ - assert optional is not None, "Unwrap called on None" + if optional is None: + raise ValueError("Unwrap called on None") return optional