Skip to content

Algorithms

ecutils.algorithms

Cryptographic algorithms built on top of the core elliptic curve primitives.

DigitalSignature dataclass

ECDSA signature generation and verification.

Attributes:

Name Type Description
private_key int

The private scalar (integer).

curve_name str

Name of the curve (e.g. "secp256k1").

Source code in ecutils/algorithms/digital_signature.py
 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
@dataclass(frozen=True)
class DigitalSignature:
    """ECDSA signature generation and verification.

    Attributes:
        private_key: The private scalar (integer).
        curve_name:  Name of the curve (e.g. ``"secp256k1"``).
    """

    private_key: int
    curve_name: str = "secp256k1"

    # ----- derived properties -----

    @property
    def _curve(self):
        return get_curve(self.curve_name)

    @property
    def _G(self) -> Point:
        return get_generator(self.curve_name)

    @property
    def public_key(self) -> Point:
        """Compute the public key: ``private_key * G``."""
        return self.private_key * self._G

    # ----- algorithm -----

    def sign(self, message_hash: int) -> tuple[int, int]:
        """Generate an ECDSA signature for a message hash.

        Algorithm:
            1. Choose random k ∈ [1, n-1]
            2. R = k·G,  r = R.x mod n
            3. s = (m + r·d) · k⁻¹ mod n   (d = private key, m = hash)

        .. warning::

           The nonce *k* must **never** be reused across different messages.
           Reusing *k* leaks the private key (as demonstrated in the
           2010 Sony PS3 ECDSA attack).

        Args:
            message_hash: Integer hash of the message (e.g. SHA-256).

        Returns:
            A tuple ``(r, s)`` representing the signature.
        """
        n = self._curve.n
        G = self._G
        r, s = 0, 0
        while r == 0 or s == 0:
            k = secrets.randbelow(n - 1) + 1
            R = k * G
            r = R.x % n
            s = ((message_hash + r * self.private_key) * pow(k, -1, n)) % n
        return r, s

    def verify(self, public_key: Point, message_hash: int, r: int, s: int) -> bool:
        """Verify an ECDSA signature.

        Algorithm:
            1. w  = s⁻¹ mod n
            2. u₁ = m·w mod n,  u₂ = r·w mod n
            3. R' = u₁·G + u₂·Q   (Q = public key)
            4. Accept iff R'.x mod n = r

        Args:
            public_key:   The signer's public key point.
            message_hash: Integer hash of the signed message.
            r:            First component of the signature.
            s:            Second component of the signature.

        Returns:
            ``True`` if the signature is valid, ``False`` otherwise.

        Raises:
            ValueError: If ``r`` or ``s`` are outside ``[1, n-1]``.
        """
        n = self._curve.n
        if not (1 <= r < n and 1 <= s < n):
            raise ValueError("r or s are not in the valid range [1, n-1].")

        G = self._G
        w = pow(s, -1, n)
        u1 = (message_hash * w) % n
        u2 = (r * w) % n
        R = u1 * G + u2 * public_key
        return R.x % n == r

    # ----- convenience wrappers -----

    def sign_message(
        self,
        message: bytes,
        hash_func: Callable[[bytes], Any] = hashlib.sha256,
    ) -> tuple[int, int]:
        """Hash a message and sign it.

        Args:
            message:   The raw message bytes to sign.
            hash_func: Hash constructor (default ``hashlib.sha256``).
                       Any callable that accepts ``bytes`` and returns an
                       object with a ``.hexdigest()`` method (e.g.
                       ``hashlib.sha384``, ``hashlib.sha512``,
                       ``hashlib.sha3_256``).

        Returns:
            A tuple ``(r, s)`` representing the ECDSA signature.
        """
        message_hash = int(hash_func(message).hexdigest(), 16)
        return self.sign(message_hash)

    def verify_message(
        self,
        public_key: Point,
        message: bytes,
        r: int,
        s: int,
        hash_func: Callable[[bytes], Any] = hashlib.sha256,
    ) -> tuple[int, int] | bool:
        """Hash a message and verify its ECDSA signature.

        Args:
            public_key: The signer's public key point.
            message:    The raw message bytes.
            r:          First component of the signature.
            s:          Second component of the signature.
            hash_func:  Hash constructor (default ``hashlib.sha256``).
                        Must match the one used in ``sign_message``.

        Returns:
            ``True`` if the signature is valid, ``False`` otherwise.
        """
        message_hash = int(hash_func(message).hexdigest(), 16)
        return self.verify(public_key, message_hash, r, s)

public_key: Point property

Compute the public key: private_key * G.

sign(message_hash)

Generate an ECDSA signature for a message hash.

Algorithm
  1. Choose random k ∈ [1, n-1]
  2. R = k·G, r = R.x mod n
  3. s = (m + r·d) · k⁻¹ mod n (d = private key, m = hash)

.. warning::

The nonce k must never be reused across different messages. Reusing k leaks the private key (as demonstrated in the 2010 Sony PS3 ECDSA attack).

Parameters:

Name Type Description Default
message_hash int

Integer hash of the message (e.g. SHA-256).

required

Returns:

Type Description
tuple[int, int]

A tuple (r, s) representing the signature.

Source code in ecutils/algorithms/digital_signature.py
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
def sign(self, message_hash: int) -> tuple[int, int]:
    """Generate an ECDSA signature for a message hash.

    Algorithm:
        1. Choose random k ∈ [1, n-1]
        2. R = k·G,  r = R.x mod n
        3. s = (m + r·d) · k⁻¹ mod n   (d = private key, m = hash)

    .. warning::

       The nonce *k* must **never** be reused across different messages.
       Reusing *k* leaks the private key (as demonstrated in the
       2010 Sony PS3 ECDSA attack).

    Args:
        message_hash: Integer hash of the message (e.g. SHA-256).

    Returns:
        A tuple ``(r, s)`` representing the signature.
    """
    n = self._curve.n
    G = self._G
    r, s = 0, 0
    while r == 0 or s == 0:
        k = secrets.randbelow(n - 1) + 1
        R = k * G
        r = R.x % n
        s = ((message_hash + r * self.private_key) * pow(k, -1, n)) % n
    return r, s

sign_message(message, hash_func=hashlib.sha256)

Hash a message and sign it.

Parameters:

Name Type Description Default
message bytes

The raw message bytes to sign.

required
hash_func Callable[[bytes], Any]

Hash constructor (default hashlib.sha256). Any callable that accepts bytes and returns an object with a .hexdigest() method (e.g. hashlib.sha384, hashlib.sha512, hashlib.sha3_256).

sha256

Returns:

Type Description
tuple[int, int]

A tuple (r, s) representing the ECDSA signature.

Source code in ecutils/algorithms/digital_signature.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def sign_message(
    self,
    message: bytes,
    hash_func: Callable[[bytes], Any] = hashlib.sha256,
) -> tuple[int, int]:
    """Hash a message and sign it.

    Args:
        message:   The raw message bytes to sign.
        hash_func: Hash constructor (default ``hashlib.sha256``).
                   Any callable that accepts ``bytes`` and returns an
                   object with a ``.hexdigest()`` method (e.g.
                   ``hashlib.sha384``, ``hashlib.sha512``,
                   ``hashlib.sha3_256``).

    Returns:
        A tuple ``(r, s)`` representing the ECDSA signature.
    """
    message_hash = int(hash_func(message).hexdigest(), 16)
    return self.sign(message_hash)

verify(public_key, message_hash, r, s)

Verify an ECDSA signature.

Algorithm
  1. w = s⁻¹ mod n
  2. u₁ = m·w mod n, u₂ = r·w mod n
  3. R' = u₁·G + u₂·Q (Q = public key)
  4. Accept iff R'.x mod n = r

Parameters:

Name Type Description Default
public_key Point

The signer's public key point.

required
message_hash int

Integer hash of the signed message.

required
r int

First component of the signature.

required
s int

Second component of the signature.

required

Returns:

Type Description
bool

True if the signature is valid, False otherwise.

Raises:

Type Description
ValueError

If r or s are outside [1, n-1].

Source code in ecutils/algorithms/digital_signature.py
 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
def verify(self, public_key: Point, message_hash: int, r: int, s: int) -> bool:
    """Verify an ECDSA signature.

    Algorithm:
        1. w  = s⁻¹ mod n
        2. u₁ = m·w mod n,  u₂ = r·w mod n
        3. R' = u₁·G + u₂·Q   (Q = public key)
        4. Accept iff R'.x mod n = r

    Args:
        public_key:   The signer's public key point.
        message_hash: Integer hash of the signed message.
        r:            First component of the signature.
        s:            Second component of the signature.

    Returns:
        ``True`` if the signature is valid, ``False`` otherwise.

    Raises:
        ValueError: If ``r`` or ``s`` are outside ``[1, n-1]``.
    """
    n = self._curve.n
    if not (1 <= r < n and 1 <= s < n):
        raise ValueError("r or s are not in the valid range [1, n-1].")

    G = self._G
    w = pow(s, -1, n)
    u1 = (message_hash * w) % n
    u2 = (r * w) % n
    R = u1 * G + u2 * public_key
    return R.x % n == r

verify_message(public_key, message, r, s, hash_func=hashlib.sha256)

Hash a message and verify its ECDSA signature.

Parameters:

Name Type Description Default
public_key Point

The signer's public key point.

required
message bytes

The raw message bytes.

required
r int

First component of the signature.

required
s int

Second component of the signature.

required
hash_func Callable[[bytes], Any]

Hash constructor (default hashlib.sha256). Must match the one used in sign_message.

sha256

Returns:

Type Description
tuple[int, int] | bool

True if the signature is valid, False otherwise.

Source code in ecutils/algorithms/digital_signature.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def verify_message(
    self,
    public_key: Point,
    message: bytes,
    r: int,
    s: int,
    hash_func: Callable[[bytes], Any] = hashlib.sha256,
) -> tuple[int, int] | bool:
    """Hash a message and verify its ECDSA signature.

    Args:
        public_key: The signer's public key point.
        message:    The raw message bytes.
        r:          First component of the signature.
        s:          Second component of the signature.
        hash_func:  Hash constructor (default ``hashlib.sha256``).
                    Must match the one used in ``sign_message``.

    Returns:
        ``True`` if the signature is valid, ``False`` otherwise.
    """
    message_hash = int(hash_func(message).hexdigest(), 16)
    return self.verify(public_key, message_hash, r, s)

Koblitz dataclass

Koblitz message encoding/decoding on an elliptic curve.

Encoding algorithm
  1. Convert the message string to an integer m using base-α positional encoding (α = alphabet_size).
  2. For j = 1, 2, ..., d-1 compute x = d·m + j (mod p).
  3. Test if x³ + ax + b is a quadratic residue mod p (see :func:ecutils.utils.math.is_quadratic_residue).
  4. If yes, compute y = √(x³ + ax + b) mod p and return (Point(x, y), j).

With d = 100 the probability of failure per attempt is ≈ 1/2, so the overall failure probability after 99 attempts is ≈ 2⁻⁹⁹.

Attributes:

Name Type Description
curve_name str

Name of the curve (e.g. "secp521r1"). Larger curves can encode longer messages in a single point.

alphabet_size int

Character set size. 256 for ASCII, 65536 for Unicode.

Source code in ecutils/algorithms/koblitz.py
 23
 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
@dataclass(frozen=True)
class Koblitz:
    """Koblitz message encoding/decoding on an elliptic curve.

    Encoding algorithm:
        1. Convert the message string to an integer *m* using base-α
           positional encoding (α = alphabet_size).
        2. For j = 1, 2, ..., d-1 compute x = d·m + j (mod p).
        3. Test if x³ + ax + b is a quadratic residue mod p (see
           :func:`ecutils.utils.math.is_quadratic_residue`).
        4. If yes, compute y = √(x³ + ax + b) mod p and return
           ``(Point(x, y), j)``.

    With d = 100 the probability of failure per attempt is ≈ 1/2, so the
    overall failure probability after 99 attempts is ≈ 2⁻⁹⁹.

    Attributes:
        curve_name: Name of the curve (e.g. ``"secp521r1"``).
                    Larger curves can encode longer messages in a single point.
        alphabet_size: Character set size. 256 for ASCII, 65536 for Unicode.
    """

    curve_name: str = "secp521r1"
    alphabet_size: int = 256

    # ----- derived properties -----

    @property
    def _curve(self):
        return get_curve(self.curve_name)

    # ----- encode -----

    def encode(self, message: str) -> tuple[Point, int]:
        """Encode a text message into a point on the curve.

        The message is first converted to an integer, then Koblitz's
        method is used to find a valid curve point derived from that integer.

        Args:
            message: The text to encode (limited by curve size).

        Returns:
            A tuple ``(point, j)`` where ``j`` is needed for decoding.

        Raises:
            ValueError: If no valid point is found (extremely unlikely).
        """
        curve = self._curve
        alpha = self.alphabet_size

        # Convert string -> integer  (base-alpha encoding)
        m = sum(ord(ch) * (alpha**i) for i, ch in enumerate(message))

        # Koblitz: try x = d*m + j until y² = x³ + ax + b has a root
        d = 100
        for j in range(1, d):
            x = (d * m + j) % curve.p
            s = (pow(x, 3, curve.p) + curve.a * x + curve.b) % curve.p

            # Euler criterion + Tonelli-like shortcut for p ≡ 3 (mod 4)
            if s == pow(s, (curve.p + 1) // 2, curve.p):
                y = pow(s, (curve.p + 1) // 4, curve.p)
                pt = Point(x, y, curve)
                if pt.is_on_curve():
                    return pt, j

        raise ValueError("Failed to encode message to a valid point on the curve.")

    # ----- decode -----

    def decode(self, point: Point, j: int) -> str:
        """Decode a curve point back into the original text message.

        Args:
            point: The encoded point (as returned by :meth:`encode`).
            j:     The auxiliary value returned alongside the point.

        Returns:
            The original text message.
        """
        alpha = self.alphabet_size
        d = 100
        m = (point.x - j) // d

        chars: list[str] = []
        while m > 0:
            chars.append(chr(m % alpha))
            m //= alpha
        return "".join(chars)

decode(point, j)

Decode a curve point back into the original text message.

Parameters:

Name Type Description Default
point Point

The encoded point (as returned by :meth:encode).

required
j int

The auxiliary value returned alongside the point.

required

Returns:

Type Description
str

The original text message.

Source code in ecutils/algorithms/koblitz.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def decode(self, point: Point, j: int) -> str:
    """Decode a curve point back into the original text message.

    Args:
        point: The encoded point (as returned by :meth:`encode`).
        j:     The auxiliary value returned alongside the point.

    Returns:
        The original text message.
    """
    alpha = self.alphabet_size
    d = 100
    m = (point.x - j) // d

    chars: list[str] = []
    while m > 0:
        chars.append(chr(m % alpha))
        m //= alpha
    return "".join(chars)

encode(message)

Encode a text message into a point on the curve.

The message is first converted to an integer, then Koblitz's method is used to find a valid curve point derived from that integer.

Parameters:

Name Type Description Default
message str

The text to encode (limited by curve size).

required

Returns:

Type Description
tuple[Point, int]

A tuple (point, j) where j is needed for decoding.

Raises:

Type Description
ValueError

If no valid point is found (extremely unlikely).

Source code in ecutils/algorithms/koblitz.py
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
def encode(self, message: str) -> tuple[Point, int]:
    """Encode a text message into a point on the curve.

    The message is first converted to an integer, then Koblitz's
    method is used to find a valid curve point derived from that integer.

    Args:
        message: The text to encode (limited by curve size).

    Returns:
        A tuple ``(point, j)`` where ``j`` is needed for decoding.

    Raises:
        ValueError: If no valid point is found (extremely unlikely).
    """
    curve = self._curve
    alpha = self.alphabet_size

    # Convert string -> integer  (base-alpha encoding)
    m = sum(ord(ch) * (alpha**i) for i, ch in enumerate(message))

    # Koblitz: try x = d*m + j until y² = x³ + ax + b has a root
    d = 100
    for j in range(1, d):
        x = (d * m + j) % curve.p
        s = (pow(x, 3, curve.p) + curve.a * x + curve.b) % curve.p

        # Euler criterion + Tonelli-like shortcut for p ≡ 3 (mod 4)
        if s == pow(s, (curve.p + 1) // 2, curve.p):
            y = pow(s, (curve.p + 1) // 4, curve.p)
            pt = Point(x, y, curve)
            if pt.is_on_curve():
                return pt, j

    raise ValueError("Failed to encode message to a valid point on the curve.")