Skip to content

Core

ecutils.core

CoordinateSystem

Bases: Enum

Coordinate system used for internal arithmetic.

Two systems are supported:

  • AFFINE — points are (x, y). Each operation requires one modular inversion, which is simple but relatively slow.
  • JACOBIAN — points are (X, Y, Z) with x = X/Z², y = Y/Z³. Avoids inversions during addition/doubling (~3x faster for scalar multiplication) at the cost of a single inversion when converting back to affine form.
Source code in ecutils/core/curve.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CoordinateSystem(Enum):
    """Coordinate system used for internal arithmetic.

    Two systems are supported:

    - **AFFINE** — points are (x, y).  Each operation requires one modular
      inversion, which is simple but relatively slow.
    - **JACOBIAN** — points are (X, Y, Z) with x = X/Z², y = Y/Z³.
      Avoids inversions during addition/doubling (~3x faster for scalar
      multiplication) at the cost of a single inversion when converting
      back to affine form.
    """

    AFFINE = auto()
    JACOBIAN = auto()

CurveParams dataclass

Immutable parameters defining an elliptic curve y² = x³ + ax + b (mod p).

A valid (non-singular) elliptic curve requires a non-zero discriminant:

Δ = -16(4a³ + 27b²) ≠ 0  (mod p)

This is verified automatically at construction time; attempting to create a CurveParams with 4a³ + 27b² ≡ 0 (mod p) raises ValueError.

.. note::

For cryptographic security the group order n should be at least 2¹⁶⁰ (see NIST SP 800-57). This library does not enforce a minimum order so that small "toy" curves can be used for educational purposes.

Attributes:

Name Type Description
p int

Prime order of the finite field.

a int

Coefficient 'a' in the curve equation.

b int

Coefficient 'b' in the curve equation.

n int

Order of the generator point.

h int

Cofactor.

coord CoordinateSystem

Coordinate system used for internal computations.

Source code in ecutils/core/curve.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@dataclass(frozen=True)
class CurveParams:
    """Immutable parameters defining an elliptic curve y² = x³ + ax + b (mod p).

    A valid (non-singular) elliptic curve requires a non-zero discriminant:

        Δ = -16(4a³ + 27b²) ≠ 0  (mod p)

    This is verified automatically at construction time; attempting to
    create a ``CurveParams`` with 4a³ + 27b² ≡ 0 (mod p) raises
    ``ValueError``.

    .. note::

       For cryptographic security the group order *n* should be at
       least 2¹⁶⁰ (see NIST SP 800-57).  This library does not
       enforce a minimum order so that small "toy" curves can be used
       for educational purposes.

    Attributes:
        p: Prime order of the finite field.
        a: Coefficient 'a' in the curve equation.
        b: Coefficient 'b' in the curve equation.
        n: Order of the generator point.
        h: Cofactor.
        coord: Coordinate system used for internal computations.
    """

    p: int
    a: int
    b: int
    n: int
    h: int = 1
    coord: CoordinateSystem = CoordinateSystem.JACOBIAN

    def __post_init__(self) -> None:
        """Validate that the curve is non-singular: 4a³ + 27b² ≠ 0 (mod p)."""
        discriminant = (
            4 * pow(self.a, 3, self.p) + 27 * pow(self.b, 2, self.p)
        ) % self.p
        if discriminant == 0:
            raise ValueError(
                f"Singular curve: 4a³ + 27b² ≡ 0 (mod p) for a={self.a}, b={self.b}, p={self.p}. "
                "The discriminant must be non-zero for a valid elliptic curve."
            )

__post_init__()

Validate that the curve is non-singular: 4a³ + 27b² ≠ 0 (mod p).

Source code in ecutils/core/curve.py
63
64
65
66
67
68
69
70
71
72
def __post_init__(self) -> None:
    """Validate that the curve is non-singular: 4a³ + 27b² ≠ 0 (mod p)."""
    discriminant = (
        4 * pow(self.a, 3, self.p) + 27 * pow(self.b, 2, self.p)
    ) % self.p
    if discriminant == 0:
        raise ValueError(
            f"Singular curve: 4a³ + 27b² ≡ 0 (mod p) for a={self.a}, b={self.b}, p={self.p}. "
            "The discriminant must be non-zero for a valid elliptic curve."
        )

Point dataclass

A point on an elliptic curve that supports arithmetic operators.

Usage

curve = CurveParams(p=23, a=1, b=1, n=28, h=1) P = Point(x=0, y=1, curve=curve) Q = Point(x=6, y=19, curve=curve) P + Q # point addition 5 * P # scalar multiplication -P # point negation P == Q # equality

To switch coordinate systems, create a new CurveParams with a different coord field:

affine_curve = CurveParams(p=23, a=1, b=1, n=28, coord=CoordinateSystem.AFFINE) P_affine = Point(x=0, y=1, curve=affine_curve)

Source code in ecutils/core/point.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
@dataclass(frozen=True)
class Point:
    """A point on an elliptic curve that supports arithmetic operators.

    Usage
    -----
    >>> curve = CurveParams(p=23, a=1, b=1, n=28, h=1)
    >>> P = Point(x=0, y=1, curve=curve)
    >>> Q = Point(x=6, y=19, curve=curve)
    >>> P + Q          # point addition
    >>> 5 * P          # scalar multiplication
    >>> -P             # point negation
    >>> P == Q         # equality

    To switch coordinate systems, create a new CurveParams with a
    different ``coord`` field:

    >>> affine_curve = CurveParams(p=23, a=1, b=1, n=28, coord=CoordinateSystem.AFFINE)
    >>> P_affine = Point(x=0, y=1, curve=affine_curve)
    """

    x: int | None = None
    y: int | None = None
    curve: CurveParams | None = field(default=None, repr=False, compare=False)
    _trusted: bool = field(default=False, repr=False, compare=False)

    def __post_init__(self) -> None:
        # Points produced by internal arithmetic are already valid.
        if self._trusted:
            return
        # Identity point (point at infinity) is always valid.
        if self.x is None or self.y is None:
            return
        # Without curve params we can't validate — skip silently.
        if self.curve is None:
            return
        lhs = pow(self.y, 2, self.curve.p)
        rhs = (
            pow(self.x, 3, self.curve.p) + self.curve.a * self.x + self.curve.b
        ) % self.curve.p
        if lhs != rhs:
            raise ValueError(
                f"Point({self.x}, {self.y}) is not on the curve "
                f"y² = x³ + {self.curve.a}x + {self.curve.b} (mod {self.curve.p})."
            )

    # ----- helpers -----

    @property
    def is_identity(self) -> bool:
        """True when this point represents the point at infinity (identity element)."""
        return self.x is None or self.y is None

    def is_on_curve(self) -> bool:
        """Check whether this point satisfies y² ≡ x³ + ax + b (mod p)."""
        if self.is_identity or self.curve is None:
            return False
        lhs = pow(self.y, 2, self.curve.p)  # type: ignore[arg-type]
        rhs = (
            pow(self.x, 3, self.curve.p) + self.curve.a * self.x + self.curve.b  # type: ignore[operator]
        ) % self.curve.p
        return lhs == rhs

    def _require_curve(self) -> CurveParams:
        if self.curve is None:
            raise ValueError(
                "Cannot perform arithmetic on a Point without curve parameters. "
                "Pass a CurveParams instance via the 'curve' argument."
            )
        return self.curve

    def _coerce(self, other: Point) -> Point:
        """Ensure *other* carries curve params (borrow ours if needed)."""
        if other.curve is None and self.curve is not None:
            return Point(other.x, other.y, self.curve)
        return other

    def _wrap(self, x: int | None, y: int | None) -> Point:
        return Point(x, y, self.curve, _trusted=True)

    # ----- compression -----

    def compress(self) -> tuple[int, int]:
        """Compress this point to its x-coordinate and parity bit.

        Returns:
            A tuple ``(x, parity)`` where *parity* is ``y % 2``.

        Raises:
            ValueError: If this point is the identity (point at infinity).
        """
        if self.is_identity:
            raise ValueError("Cannot compress the identity point (point at infinity).")
        return (self.x, self.y % 2)  # type: ignore[operator]

    @classmethod
    def decompress(cls, x: int, parity: int, curve: CurveParams) -> Point:
        """Reconstruct a point from its compressed form.

        Args:
            x:      The x-coordinate.
            parity: The parity bit (0 or 1) indicating which y to select.
            curve:  The curve parameters.

        Returns:
            The decompressed ``Point``.

        Raises:
            ValueError: If *x* does not correspond to a valid point on the curve.
        """
        rhs = (pow(x, 3, curve.p) + curve.a * x + curve.b) % curve.p
        y = modular_sqrt(rhs, curve.p)
        if y is None:
            raise ValueError(
                f"x={x} does not correspond to a valid point on the curve "
                f"y² = x³ + {curve.a}x + {curve.b} (mod {curve.p})."
            )
        if y % 2 != parity:
            y = curve.p - y
        return cls(x, y, curve)

    # ----- SEC 1 compression (interoperable) -----

    def compress_sec1(self) -> bytes:
        """Compress this point to SEC 1 / X9.62 format.

        The output is a single byte prefix (``0x02`` for even y, ``0x03``
        for odd y) followed by the x-coordinate as a big-endian unsigned
        integer, zero-padded to the field size.

        Returns:
            Compressed point as bytes.

        Raises:
            ValueError: If this point is the identity or has no curve params.
        """
        if self.is_identity:
            raise ValueError("Cannot compress the identity point (point at infinity).")
        curve = self._require_curve()
        byte_len = (curve.p.bit_length() + 7) // 8
        prefix = b"\x03" if self.y % 2 else b"\x02"  # type: ignore[operator]
        return prefix + self.x.to_bytes(byte_len, "big")  # type: ignore[union-attr]

    def to_uncompressed_sec1(self) -> bytes:
        """Serialize this point to SEC 1 / X9.62 uncompressed format.

        The output is ``0x04 || x || y``, where x and y are big-endian
        unsigned integers zero-padded to the field size.

        Returns:
            Uncompressed point as bytes.

        Raises:
            ValueError: If this point is the identity or has no curve params.
        """
        if self.is_identity:
            raise ValueError("Cannot serialize the identity point (point at infinity).")
        curve = self._require_curve()
        byte_len = (curve.p.bit_length() + 7) // 8
        return (
            b"\x04"
            + self.x.to_bytes(byte_len, "big")  # type: ignore[union-attr]
            + self.y.to_bytes(byte_len, "big")  # type: ignore[union-attr]
        )

    @classmethod
    def from_sec1(cls, data: bytes, curve: CurveParams) -> Point:
        """Deserialize a point from SEC 1 / X9.62 format.

        Supports both compressed (``0x02``/``0x03`` prefix) and
        uncompressed (``0x04`` prefix) encodings.

        Args:
            data:  The SEC 1 encoded point bytes.
            curve: The curve parameters.

        Returns:
            The deserialized ``Point``.

        Raises:
            ValueError: If the data is malformed or the point is invalid.
        """
        if len(data) < 2:
            raise ValueError("SEC 1 data too short.")
        byte_len = (curve.p.bit_length() + 7) // 8
        prefix = data[0]

        if prefix in (0x02, 0x03):
            if len(data) != 1 + byte_len:
                raise ValueError(
                    f"Compressed SEC 1 data must be {1 + byte_len} bytes, "
                    f"got {len(data)}."
                )
            x = int.from_bytes(data[1:], "big")
            parity = prefix - 0x02  # 0 for even, 1 for odd
            return cls.decompress(x, parity, curve)

        if prefix == 0x04:
            if len(data) != 1 + 2 * byte_len:
                raise ValueError(
                    f"Uncompressed SEC 1 data must be {1 + 2 * byte_len} bytes, "
                    f"got {len(data)}."
                )
            x = int.from_bytes(data[1 : 1 + byte_len], "big")
            y = int.from_bytes(data[1 + byte_len :], "big")
            return cls(x, y, curve)

        raise ValueError(
            f"Unknown SEC 1 prefix: 0x{prefix:02x}. Expected 0x02, 0x03, or 0x04."
        )

    # ----- operators -----

    def __neg__(self) -> Point:
        """Return the additive inverse: -P = (x, -y mod p)."""
        if self.is_identity:
            return self
        curve = self._require_curve()
        return Point(self.x, (-self.y) % curve.p, curve, _trusted=True)  # type: ignore[operator]

    def __add__(self, other: Point) -> Point:
        """Add two points on the same curve using the group law.

        Delegates to affine or Jacobian arithmetic depending on
        ``curve.coord``.  The chord-and-tangent formulas are:

        Addition (P ≠ Q):
            λ  = (y₂ - y₁) · (x₂ - x₁)⁻¹
            x₃ = λ² - x₁ - x₂
            y₃ = λ(x₁ - x₃) - y₁

        Doubling (P = Q):
            λ  = (3x₁² + a) · (2y₁)⁻¹
            x₃ = λ² - 2x₁
            y₃ = λ(x₁ - x₃) - y₁
        """
        curve = self._require_curve()
        other = self._coerce(other)

        if curve.coord is CoordinateSystem.JACOBIAN:
            jp1 = to_jacobian(self)
            jp2 = to_jacobian(other)
            jp3 = jac_add(jp1, jp2, curve)
            return self._wrap(*to_affine(jp3, curve))

        return self._wrap(*affine_add(self.x, self.y, other.x, other.y, curve))

    def __sub__(self, other: Point) -> Point:
        """Subtract: self + (-other)."""
        return self.__add__(-other)

    def __mul__(self, k: int) -> Point:
        """Scalar multiplication: k · P via double-and-add in O(log k)."""
        curve = self._require_curve()
        k = k % curve.n

        if curve.coord is CoordinateSystem.JACOBIAN:
            jp = to_jacobian(self)
            jp_result = jac_mul(k, jp, curve)
            return self._wrap(*to_affine(jp_result, curve))

        return self._wrap(*affine_mul(k, self.x, self.y, curve))

    def __rmul__(self, k: int) -> Point:
        """Scalar multiplication: k * Point."""
        return self.__mul__(k)

    # ----- display -----

    def __repr__(self) -> str:
        if self.is_identity:
            return "Point(∞)"
        return f"Point(x={self.x}, y={self.y})"

is_identity: bool property

True when this point represents the point at infinity (identity element).

__add__(other)

Add two points on the same curve using the group law.

Delegates to affine or Jacobian arithmetic depending on curve.coord. The chord-and-tangent formulas are:

Addition (P ≠ Q): λ = (y₂ - y₁) · (x₂ - x₁)⁻¹ x₃ = λ² - x₁ - x₂ y₃ = λ(x₁ - x₃) - y₁

Doubling (P = Q): λ = (3x₁² + a) · (2y₁)⁻¹ x₃ = λ² - 2x₁ y₃ = λ(x₁ - x₃) - y₁

Source code in ecutils/core/point.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def __add__(self, other: Point) -> Point:
    """Add two points on the same curve using the group law.

    Delegates to affine or Jacobian arithmetic depending on
    ``curve.coord``.  The chord-and-tangent formulas are:

    Addition (P ≠ Q):
        λ  = (y₂ - y₁) · (x₂ - x₁)⁻¹
        x₃ = λ² - x₁ - x₂
        y₃ = λ(x₁ - x₃) - y₁

    Doubling (P = Q):
        λ  = (3x₁² + a) · (2y₁)⁻¹
        x₃ = λ² - 2x₁
        y₃ = λ(x₁ - x₃) - y₁
    """
    curve = self._require_curve()
    other = self._coerce(other)

    if curve.coord is CoordinateSystem.JACOBIAN:
        jp1 = to_jacobian(self)
        jp2 = to_jacobian(other)
        jp3 = jac_add(jp1, jp2, curve)
        return self._wrap(*to_affine(jp3, curve))

    return self._wrap(*affine_add(self.x, self.y, other.x, other.y, curve))

__mul__(k)

Scalar multiplication: k · P via double-and-add in O(log k).

Source code in ecutils/core/point.py
275
276
277
278
279
280
281
282
283
284
285
def __mul__(self, k: int) -> Point:
    """Scalar multiplication: k · P via double-and-add in O(log k)."""
    curve = self._require_curve()
    k = k % curve.n

    if curve.coord is CoordinateSystem.JACOBIAN:
        jp = to_jacobian(self)
        jp_result = jac_mul(k, jp, curve)
        return self._wrap(*to_affine(jp_result, curve))

    return self._wrap(*affine_mul(k, self.x, self.y, curve))

__neg__()

Return the additive inverse: -P = (x, -y mod p).

Source code in ecutils/core/point.py
237
238
239
240
241
242
def __neg__(self) -> Point:
    """Return the additive inverse: -P = (x, -y mod p)."""
    if self.is_identity:
        return self
    curve = self._require_curve()
    return Point(self.x, (-self.y) % curve.p, curve, _trusted=True)  # type: ignore[operator]

__rmul__(k)

Scalar multiplication: k * Point.

Source code in ecutils/core/point.py
287
288
289
def __rmul__(self, k: int) -> Point:
    """Scalar multiplication: k * Point."""
    return self.__mul__(k)

__sub__(other)

Subtract: self + (-other).

Source code in ecutils/core/point.py
271
272
273
def __sub__(self, other: Point) -> Point:
    """Subtract: self + (-other)."""
    return self.__add__(-other)

compress()

Compress this point to its x-coordinate and parity bit.

Returns:

Type Description
tuple[int, int]

A tuple (x, parity) where parity is y % 2.

Raises:

Type Description
ValueError

If this point is the identity (point at infinity).

Source code in ecutils/core/point.py
106
107
108
109
110
111
112
113
114
115
116
117
def compress(self) -> tuple[int, int]:
    """Compress this point to its x-coordinate and parity bit.

    Returns:
        A tuple ``(x, parity)`` where *parity* is ``y % 2``.

    Raises:
        ValueError: If this point is the identity (point at infinity).
    """
    if self.is_identity:
        raise ValueError("Cannot compress the identity point (point at infinity).")
    return (self.x, self.y % 2)  # type: ignore[operator]

compress_sec1()

Compress this point to SEC 1 / X9.62 format.

The output is a single byte prefix (0x02 for even y, 0x03 for odd y) followed by the x-coordinate as a big-endian unsigned integer, zero-padded to the field size.

Returns:

Type Description
bytes

Compressed point as bytes.

Raises:

Type Description
ValueError

If this point is the identity or has no curve params.

Source code in ecutils/core/point.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def compress_sec1(self) -> bytes:
    """Compress this point to SEC 1 / X9.62 format.

    The output is a single byte prefix (``0x02`` for even y, ``0x03``
    for odd y) followed by the x-coordinate as a big-endian unsigned
    integer, zero-padded to the field size.

    Returns:
        Compressed point as bytes.

    Raises:
        ValueError: If this point is the identity or has no curve params.
    """
    if self.is_identity:
        raise ValueError("Cannot compress the identity point (point at infinity).")
    curve = self._require_curve()
    byte_len = (curve.p.bit_length() + 7) // 8
    prefix = b"\x03" if self.y % 2 else b"\x02"  # type: ignore[operator]
    return prefix + self.x.to_bytes(byte_len, "big")  # type: ignore[union-attr]

decompress(x, parity, curve) classmethod

Reconstruct a point from its compressed form.

Parameters:

Name Type Description Default
x int

The x-coordinate.

required
parity int

The parity bit (0 or 1) indicating which y to select.

required
curve CurveParams

The curve parameters.

required

Returns:

Type Description
Point

The decompressed Point.

Raises:

Type Description
ValueError

If x does not correspond to a valid point on the curve.

Source code in ecutils/core/point.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
@classmethod
def decompress(cls, x: int, parity: int, curve: CurveParams) -> Point:
    """Reconstruct a point from its compressed form.

    Args:
        x:      The x-coordinate.
        parity: The parity bit (0 or 1) indicating which y to select.
        curve:  The curve parameters.

    Returns:
        The decompressed ``Point``.

    Raises:
        ValueError: If *x* does not correspond to a valid point on the curve.
    """
    rhs = (pow(x, 3, curve.p) + curve.a * x + curve.b) % curve.p
    y = modular_sqrt(rhs, curve.p)
    if y is None:
        raise ValueError(
            f"x={x} does not correspond to a valid point on the curve "
            f"y² = x³ + {curve.a}x + {curve.b} (mod {curve.p})."
        )
    if y % 2 != parity:
        y = curve.p - y
    return cls(x, y, curve)

from_sec1(data, curve) classmethod

Deserialize a point from SEC 1 / X9.62 format.

Supports both compressed (0x02/0x03 prefix) and uncompressed (0x04 prefix) encodings.

Parameters:

Name Type Description Default
data bytes

The SEC 1 encoded point bytes.

required
curve CurveParams

The curve parameters.

required

Returns:

Type Description
Point

The deserialized Point.

Raises:

Type Description
ValueError

If the data is malformed or the point is invalid.

Source code in ecutils/core/point.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
@classmethod
def from_sec1(cls, data: bytes, curve: CurveParams) -> Point:
    """Deserialize a point from SEC 1 / X9.62 format.

    Supports both compressed (``0x02``/``0x03`` prefix) and
    uncompressed (``0x04`` prefix) encodings.

    Args:
        data:  The SEC 1 encoded point bytes.
        curve: The curve parameters.

    Returns:
        The deserialized ``Point``.

    Raises:
        ValueError: If the data is malformed or the point is invalid.
    """
    if len(data) < 2:
        raise ValueError("SEC 1 data too short.")
    byte_len = (curve.p.bit_length() + 7) // 8
    prefix = data[0]

    if prefix in (0x02, 0x03):
        if len(data) != 1 + byte_len:
            raise ValueError(
                f"Compressed SEC 1 data must be {1 + byte_len} bytes, "
                f"got {len(data)}."
            )
        x = int.from_bytes(data[1:], "big")
        parity = prefix - 0x02  # 0 for even, 1 for odd
        return cls.decompress(x, parity, curve)

    if prefix == 0x04:
        if len(data) != 1 + 2 * byte_len:
            raise ValueError(
                f"Uncompressed SEC 1 data must be {1 + 2 * byte_len} bytes, "
                f"got {len(data)}."
            )
        x = int.from_bytes(data[1 : 1 + byte_len], "big")
        y = int.from_bytes(data[1 + byte_len :], "big")
        return cls(x, y, curve)

    raise ValueError(
        f"Unknown SEC 1 prefix: 0x{prefix:02x}. Expected 0x02, 0x03, or 0x04."
    )

is_on_curve()

Check whether this point satisfies y² ≡ x³ + ax + b (mod p).

Source code in ecutils/core/point.py
77
78
79
80
81
82
83
84
85
def is_on_curve(self) -> bool:
    """Check whether this point satisfies y² ≡ x³ + ax + b (mod p)."""
    if self.is_identity or self.curve is None:
        return False
    lhs = pow(self.y, 2, self.curve.p)  # type: ignore[arg-type]
    rhs = (
        pow(self.x, 3, self.curve.p) + self.curve.a * self.x + self.curve.b  # type: ignore[operator]
    ) % self.curve.p
    return lhs == rhs

to_uncompressed_sec1()

Serialize this point to SEC 1 / X9.62 uncompressed format.

The output is 0x04 || x || y, where x and y are big-endian unsigned integers zero-padded to the field size.

Returns:

Type Description
bytes

Uncompressed point as bytes.

Raises:

Type Description
ValueError

If this point is the identity or has no curve params.

Source code in ecutils/core/point.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def to_uncompressed_sec1(self) -> bytes:
    """Serialize this point to SEC 1 / X9.62 uncompressed format.

    The output is ``0x04 || x || y``, where x and y are big-endian
    unsigned integers zero-padded to the field size.

    Returns:
        Uncompressed point as bytes.

    Raises:
        ValueError: If this point is the identity or has no curve params.
    """
    if self.is_identity:
        raise ValueError("Cannot serialize the identity point (point at infinity).")
    curve = self._require_curve()
    byte_len = (curve.p.bit_length() + 7) // 8
    return (
        b"\x04"
        + self.x.to_bytes(byte_len, "big")  # type: ignore[union-attr]
        + self.y.to_bytes(byte_len, "big")  # type: ignore[union-attr]
    )

affine_add(p1x, p1y, p2x, p2y, curve) cached

Add two distinct points in affine coordinates.

Given P₁ = (x₁, y₁) and P₂ = (x₂, y₂) with P₁ ≠ P₂, the chord-line formula is:

λ  = (y₂ - y₁) · (x₂ - x₁)⁻¹  (mod p)
x₃ = λ² - x₁ - x₂              (mod p)
y₃ = λ(x₁ - x₃) - y₁           (mod p)

If P₁ = P₂ the call is forwarded to :func:affine_double.

Example (E: y² = x³ + x + 1 over F₂₃, P(0,1) + Q(6,19)):

>>> curve = CurveParams(p=23, a=1, b=1, n=28, h=1, coord=CoordinateSystem.AFFINE)
>>> affine_add(0, 1, 6, 19, curve)
(3, 13)
Source code in ecutils/core/arithmetic/affine.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
@lru_cache(maxsize=LRU_CACHE_MAXSIZE)
def affine_add(
    p1x: int | None,
    p1y: int | None,
    p2x: int | None,
    p2y: int | None,
    curve: CurveParams,
) -> tuple[int | None, int | None]:
    """Add two distinct points in affine coordinates.

    Given P₁ = (x₁, y₁) and P₂ = (x₂, y₂) with P₁ ≠ P₂, the chord-line
    formula is:

        λ  = (y₂ - y₁) · (x₂ - x₁)⁻¹  (mod p)
        x₃ = λ² - x₁ - x₂              (mod p)
        y₃ = λ(x₁ - x₃) - y₁           (mod p)

    If P₁ = P₂ the call is forwarded to :func:`affine_double`.

    Example (E: y² = x³ + x + 1 over F₂₃, P(0,1) + Q(6,19)):

        >>> curve = CurveParams(p=23, a=1, b=1, n=28, h=1, coord=CoordinateSystem.AFFINE)
        >>> affine_add(0, 1, 6, 19, curve)
        (3, 13)
    """
    if p1x is None or p1y is None:
        return (p2x, p2y)
    if p2x is None or p2y is None:
        return (p1x, p1y)
    if p1x == p2x and p1y == p2y:
        return affine_double(p1x, p1y, curve)
    p = curve.p
    n = (p2y - p1y) % p
    d = (p2x - p1x) % p
    try:
        inv = pow(d, -1, p)
    except ValueError:
        return (None, None)
    s = n * inv % p
    x3 = (s**2 - p1x - p2x) % p
    y3 = (s * (p1x - x3) - p1y) % p
    return (x3, y3)

affine_double(px, py, curve) cached

Double a point in affine coordinates.

Computes 2P using the tangent-line formula:

λ = (3x₁² + a) · (2y₁)⁻¹  (mod p)
x₃ = λ² - 2x₁              (mod p)
y₃ = λ(x₁ - x₃) - y₁      (mod p)

Example (E: y² = x³ + x + 1 over F₂₃):

>>> curve = CurveParams(p=23, a=1, b=1, n=28, h=1, coord=CoordinateSystem.AFFINE)
>>> affine_double(0, 1, curve)
(6, 19)
Source code in ecutils/core/arithmetic/affine.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@lru_cache(maxsize=LRU_CACHE_MAXSIZE)
def affine_double(
    px: int | None, py: int | None, curve: CurveParams
) -> tuple[int | None, int | None]:
    """Double a point in affine coordinates.

    Computes 2P using the tangent-line formula:

        λ = (3x₁² + a) · (2y₁)⁻¹  (mod p)
        x₃ = λ² - 2x₁              (mod p)
        y₃ = λ(x₁ - x₃) - y₁      (mod p)

    Example (E: y² = x³ + x + 1 over F₂₃):

        >>> curve = CurveParams(p=23, a=1, b=1, n=28, h=1, coord=CoordinateSystem.AFFINE)
        >>> affine_double(0, 1, curve)
        (6, 19)
    """
    if px is None or py is None:
        return (None, None)
    p = curve.p
    n = (3 * px**2 + curve.a) % p
    d = (2 * py) % p
    try:
        inv = pow(d, -1, p)
    except ValueError:
        return (None, None)
    s = n * inv % p
    x3 = (s**2 - 2 * px) % p
    y3 = (s * (px - x3) - py) % p
    return (x3, y3)

affine_mul(k, px, py, curve)

Scalar multiplication in affine coordinates (double-and-add).

Computes k·P by scanning the bits of k from LSB to MSB, accumulating the result and doubling the base at each step. Runs in O(log k) doublings and at most O(log k) additions.

.. warning::

The double-and-add algorithm is not constant-time: the number of additions depends on the Hamming weight of k. For constant-time requirements see RFC 6090, Section 4.

Source code in ecutils/core/arithmetic/affine.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def affine_mul(
    k: int, px: int | None, py: int | None, curve: CurveParams
) -> tuple[int | None, int | None]:
    """Scalar multiplication in affine coordinates (double-and-add).

    Computes k·P by scanning the bits of *k* from LSB to MSB,
    accumulating the result and doubling the base at each step.
    Runs in O(log k) doublings and at most O(log k) additions.

    .. warning::

       The double-and-add algorithm is **not** constant-time: the number
       of additions depends on the Hamming weight of *k*.  For
       constant-time requirements see RFC 6090, Section 4.
    """
    if px is None or py is None or k == 0:
        return (None, None)
    rx: int | None = None
    ry: int | None = None
    while k > 0:
        if k & 1:
            rx, ry = affine_add(rx, ry, px, py, curve)
        px, py = affine_double(px, py, curve)
        k >>= 1
    return (rx, ry)

jac_add(jp1, jp2, curve) cached

Add two points in Jacobian coordinates.

Uses the standard Jacobian addition formulas (see RFC 6090 Section 4):

U₁ = X₁·Z₂²,  U₂ = X₂·Z₁²
S₁ = Y₁·Z₂³,  S₂ = Y₂·Z₁³
H  = U₂ - U₁,  R = 2·(S₂ - S₁)
X' = R² - H³ - 2·U₁·H²
Y' = R·(U₁·H² - X') - 2·S₁·H³
Z' = ((Z₁ + Z₂)² - Z₁² - Z₂²)·H

If U₁ = U₂ and S₁ ≠ S₂ the points are inverses → identity. If U₁ = U₂ and S₁ = S₂ the points are equal → delegates to :func:jac_double.

Source code in ecutils/core/arithmetic/jacobian.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@lru_cache(maxsize=LRU_CACHE_MAXSIZE)
def jac_add(
    jp1: _JacobianPoint, jp2: _JacobianPoint, curve: CurveParams
) -> _JacobianPoint:
    """Add two points in Jacobian coordinates.

    Uses the standard Jacobian addition formulas (see RFC 6090 Section 4):

        U₁ = X₁·Z₂²,  U₂ = X₂·Z₁²
        S₁ = Y₁·Z₂³,  S₂ = Y₂·Z₁³
        H  = U₂ - U₁,  R = 2·(S₂ - S₁)
        X' = R² - H³ - 2·U₁·H²
        Y' = R·(U₁·H² - X') - 2·S₁·H³
        Z' = ((Z₁ + Z₂)² - Z₁² - Z₂²)·H

    If U₁ = U₂ and S₁ ≠ S₂ the points are inverses → identity.
    If U₁ = U₂ and S₁ = S₂ the points are equal → delegates to :func:`jac_double`.
    """
    if jp1.x is None or jp1.y is None:
        return jp2
    if jp2.x is None or jp2.y is None:
        return jp1
    p = curve.p
    z1z1 = jp1.z * jp1.z % p
    z2z2 = jp2.z * jp2.z % p
    u1 = jp1.x * z2z2 % p
    u2 = jp2.x * z1z1 % p
    s1 = jp1.y * jp2.z * z2z2 % p
    s2 = jp2.y * jp1.z * z1z1 % p
    if u1 == u2:
        if s1 != s2:
            return _JacobianPoint()
        return jac_double(jp1, curve)
    h = u2 - u1
    i = (2 * h) * (2 * h) % p
    j = h * i % p
    r = 2 * (s2 - s1) % p
    v = u1 * i % p
    x = (r * r - j - 2 * v) % p
    y = (r * (v - x) - 2 * s1 * j) % p
    z = ((jp1.z + jp2.z) ** 2 - z1z1 - z2z2) * h % p
    return _JacobianPoint(x, y, z)

jac_double(jp, curve) cached

Double a point in Jacobian coordinates.

Uses the standard Jacobian doubling formulas (see RFC 6090 Section 4):

S = 4·X·Y²
M = 3·X² + a·Z⁴
X' = M² - 2·S
Y' = M·(S - X') - 8·Y⁴
Z' = 2·Y·Z

Cost: 1S + 4M (no field inversions).

Source code in ecutils/core/arithmetic/jacobian.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@lru_cache(maxsize=LRU_CACHE_MAXSIZE)
def jac_double(jp: _JacobianPoint, curve: CurveParams) -> _JacobianPoint:
    """Double a point in Jacobian coordinates.

    Uses the standard Jacobian doubling formulas (see RFC 6090 Section 4):

        S = 4·X·Y²
        M = 3·X² + a·Z⁴
        X' = M² - 2·S
        Y' = M·(S - X') - 8·Y⁴
        Z' = 2·Y·Z

    Cost: 1S + 4M (no field inversions).
    """
    if jp.x is None or jp.y is None or jp.y == 0:
        return _JacobianPoint()
    p = curve.p
    ysq = jp.y * jp.y % p
    zsqr = jp.z * jp.z % p
    s = 4 * jp.x * ysq % p
    m = (3 * jp.x * jp.x + curve.a * zsqr * zsqr) % p
    nx = (m * m - 2 * s) % p
    ny = (m * (s - nx) - 8 * ysq * ysq) % p
    nz = 2 * jp.y * jp.z % p
    return _JacobianPoint(nx, ny, nz)

jac_mul(k, jp, curve)

Scalar multiplication in Jacobian coordinates (double-and-add).

Computes k·P using the binary expansion of k. Runs in O(log k) doublings and at most O(log k) additions, all without field inversions until the final conversion back to affine.

Source code in ecutils/core/arithmetic/jacobian.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def jac_mul(k: int, jp: _JacobianPoint, curve: CurveParams) -> _JacobianPoint:
    """Scalar multiplication in Jacobian coordinates (double-and-add).

    Computes k·P using the binary expansion of *k*.  Runs in O(log k)
    doublings and at most O(log k) additions, all without field inversions
    until the final conversion back to affine.
    """
    if k == 0 or jp.x is None or jp.y is None:
        return _JacobianPoint()
    result = _JacobianPoint()
    bits = bin(k)[2:]
    for i in range(len(bits)):
        if bits[-i - 1] == "1":
            result = jac_add(result, jp, curve)
        jp = jac_double(jp, curve)
    return result

to_affine(jp, curve)

Convert a _JacobianPoint back to affine (x, y) coordinates.

Source code in ecutils/core/arithmetic/jacobian.py
55
56
57
58
59
60
61
62
63
def to_affine(jp: _JacobianPoint, curve: CurveParams) -> tuple[int | None, int | None]:
    """Convert a _JacobianPoint back to affine (x, y) coordinates."""
    if jp.x is None or jp.y is None or jp.z == 0:
        return (None, None)
    inv_z = pow(jp.z, -1, curve.p)
    return (
        (jp.x * inv_z**2) % curve.p,
        (jp.y * inv_z**3) % curve.p,
    )

to_jacobian(pt)

Convert an affine Point to Jacobian coordinates.

Source code in ecutils/core/arithmetic/jacobian.py
48
49
50
51
52
def to_jacobian(pt: Point) -> _JacobianPoint:
    """Convert an affine Point to Jacobian coordinates."""
    if pt.is_identity:
        return _JacobianPoint()
    return _JacobianPoint(pt.x, pt.y, 1)