Skip to content

Fix identity element arithmetic and add projective form for curves over rings - #15

Merged
serengil merged 2 commits into
mainfrom
sefiks/fix-identity-element-and-add-projective-form
Oct 22, 2026
Merged

serengil merged 2 commits into
mainfrom
sefiks/fix-identity-element-and-add-projective-form

Conversation

@serengil

@serengil serengil commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

Two changes:

  1. Fix the identity element O in EllipticCurvePoint. Fixes Handle the point at infinity (identity element) in point arithmetic and ECDLP #14.
  2. Add a projective form for curves y² = x³ + b over Z_m where m does not need to be
    prime, so that LightPHE's Elliptic Curve Paillier can do its curve math in LightECC instead
    of carrying its own copy.

Version is bumped to 0.0.8.

1. Identity element fix

EllipticCurvePoint didn't treat O as a special case:

  • O / G returned n instead of 0.
  • -O, O * k, k * O and P - O raised TypeError on Weierstrass and Koblitz curves,
    because O is stored there as (inf, inf) and the field arithmetic only accepts integers.
    Edwards wasn't affected, since its identity (0, 1) is a normal point.

This broke LightPHE's Elliptic Curve ElGamal: decrypt(encrypt(0)) returned n, and
decrypting encrypt(5) * 0 raised TypeError. LightPHE had workarounds for both.

Changes:

  • New EllipticCurvePoint.is_identity(). It compares against curve.O, so it works for
    every form. __repr__ uses it now.
  • __truediv__ returns 0 for O. This check runs before the P / P = 1 shortcut.
  • __neg__ returns O for O, which also fixes P - O.
  • __mul__ returns O for O with any integer k, which also covers __rmul__.

Scalars were already reduced mod n in double_and_add, so nothing changed there. Changing
how O is stored, instead of (inf, inf), was left out of this PR. The float values no
longer reach the modular arithmetic in these operations.

2. Projective form

Why

Elliptic Curve Paillier (Galbraith, 2002) works on y² = x³ + b over Z_{n²} with n = pq. The
existing forms don't work for it:

  • They use affine (x, y) points, and their addition formulas divide. Z_{n²} is not a
    field, so division fails whenever a coordinate shares a factor with n.
  • The message points (mn : 1 : 0) reduce to the point at infinity mod n but are distinct
    mod n². They have no affine form, and LightECC has only one point at infinity, so all of
    them would become the same point.
  • The group order is the private key, so it can't go into config["n"]. If it were passed,
    double_and_add would reduce order × C to 0 × C and every message would be lost.

Using a custom Weierstrass config with p = n² was tested: decryption fails with base is not invertible for the given modulus for every message except 0. (Boneh-Goh-Nissim isn't
affected. Its modulus is prime and only its group order is composite, so it keeps using the
Weierstrass form.)

What

ec = LightECC(form_name="projective", config={"modulus": n * n, "b": b, "G": g})
  • lightecc/forms/projective.py: new Projective curve class. Points are (X : Y : Z) and the
    point at infinity is (0 : 1 : 0). Addition uses the complete formulas of
    Renes-Costello-Batina (2016), Algorithm 7 for a = 0. They work for every pair of points,
    including doubling and the point at infinity, and never divide.
    • The config requires modulus, b and G (as (x, y) or (X, Y, Z)). n is optional and is
      validated when given.
    • double_and_add never reduces the scalar mod n, so it works when the order is unknown or
      secret.
    • precompute_powers / multiply_with_powers speed up multiplication of a fixed point, such
      as the base point used in every encryption.
  • ProjectivePoint in interfaces/elliptic_curve.py: supports +, -, *, unary minus, ==,
    is_identity() and to_affine(). Two points are equal when one is a scalar multiple of the
    other, which is checked through all three 2×2 cross-products. Checking only X1·Z2 = X2·Z1
    would wrongly treat (0 : 1 : 0) and (n : 1 : 0) as the same point. There's no division,
    since brute-force ECDLP needs a known order.
  • LightECC.init: new "projective" branch. Only custom curves are accepted.
  • README: new "Projective Form for Curves over Rings" section with a runnable example.

Point of order 2

The complete formulas assume the subgroup has no point of order 2. When P − Q has order 2,
P + Q returns (0 : 0 : 0), which used to pass the on-curve check and compare equal to every
point. While writing tests, this made find_order return 2504 instead of 5004 on a test
curve. is_on_curve now rejects any triple whose coordinates share a factor with the
modulus, so this case raises PointNotOnCurve instead of giving a wrong result. Elliptic
Curve Paillier isn't affected, because its points have odd order.

Tests

  • tests/test_arithmetic.py: for Weierstrass, Edwards and Koblitz, checks (G0)/G == 0,
    (G
    n)/G == 0, G/G == 1, (G5)/G == 5, -O == O, O7 == 7*O == O, G - O == G and O + G ==
    G.
  • tests/test_projective.py:
    • group laws on secp256k1, cross-checked against affine Weierstrass results
    • fixed-point precomputation
    • order validation and find_order on a small odd-order curve
    • message points over Z_{n²}
    • the point-of-order-2 case being rejected
    • config validation

All 42 tests pass. LightPHE's full suite (235 passed, 1 skipped) also passes against this
branch, with Elliptic Curve Paillier moved to the projective form and the ElGamal
workarounds removed.

Follow-up

After 0.0.8 is published to PyPI, the LightPHE PR can merge: it requires lightecc>=0.0.8.

@serengil
serengil merged commit 846550f into main Oct 22, 2026
2 checks passed
@serengil
serengil deleted the sefiks/fix-identity-element-and-add-projective-form branch October 22, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handle the point at infinity (identity element) in point arithmetic and ECDLP

1 participant