Add comprehensive Elliptic Curve Cryptography implementation - #5
Conversation
Implemented a complete educational ECC library with: - Full elliptic curve point arithmetic (addition, scalar multiplication) - Support for secp256k1 (Bitcoin/Ethereum) and P-256 (NIST) curves - ECDH (Elliptic Curve Diffie-Hellman) key exchange protocol - ECDSA (Elliptic Curve Digital Signature Algorithm) - Comprehensive test suite with 27 passing tests - Educational examples demonstrating all features - Documentation explaining the mathematical connection between elliptic curves in monstrous moonshine and cryptography Key features: - Secure key generation using secrets module - Efficient scalar multiplication using double-and-add algorithm - Full ECDH key agreement implementation - Complete ECDSA signing and verification - Clear, readable code optimized for learning The implementation connects to the Monster group theme by exploring how elliptic curves appear in both pure mathematics (j-invariant and monstrous moonshine) and applied cryptography (ECC).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Shared point = private_key * other_public_key | ||
| shared_point = private_key * other_public_key | ||
|
|
||
| if shared_point.is_at_infinity(): | ||
| raise ValueError("Shared secret computation resulted in point at infinity") |
There was a problem hiding this comment.
Validate peer public key before ECDH multiplication
The ECDH handshake multiplies the local private key with whatever other_public_key object is provided without first checking that it lies on the expected curve or even matches self.curve (the only check performed is for infinity). Because scalar multiplication uses the curve parameters baked into the Point object, an attacker can supply a crafted point on a different or low-order curve and learn your private key modulo that small order (classic invalid-curve/small-subgroup attack), producing predictable “shared secrets” that defeat confidentiality.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR adds a comprehensive educational implementation of Elliptic Curve Cryptography (ECC) to complement the existing Monster group implementation, demonstrating the mathematical connection between both through the j-invariant and elliptic curves.
Key Changes:
- Complete ECC implementation with point arithmetic, ECDH key exchange, and ECDSA digital signatures
- Support for two standard curves: secp256k1 (Bitcoin/Ethereum) and P-256 (NIST)
- Comprehensive test suite with 27 tests covering all ECC functionality
- Educational examples demonstrating practical ECC applications
- Documentation explaining the mathematical connection to monstrous moonshine
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 13 comments.
| File | Description |
|---|---|
| elliptic_curve.py | Core ECC implementation including Point class, curve operations, ECDH, and ECDSA protocols with secure random number generation |
| test_elliptic_curve.py | Comprehensive test suite covering point arithmetic, key generation, ECDH key exchange, ECDSA signatures, and curve properties |
| example_ecc.py | Educational examples demonstrating basic operations, key generation, ECDH, ECDSA, multiple curves, and connection to moonshine theory |
| README.md | Updated documentation introducing ECC features, usage examples, and explaining the mathematical connection between Monster group and elliptic curves |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| # Generate random k for this signature | ||
| while True: | ||
| k = secrets.randbelow(n - 1) + 1 |
There was a problem hiding this comment.
The nonce k generation has the same issue as private key generation - it excludes the value n-1 from the valid range. Since secrets.randbelow(n - 1) returns values in [0, n-2], adding 1 gives [1, n-1), which excludes n-1. Change to secrets.randbelow(n) or adjust the logic to properly include n-1 in the range.
| k = secrets.randbelow(n - 1) + 1 | |
| # Generate random k uniformly in [1, n-1] | |
| k = 0 | |
| while k == 0: | |
| k = secrets.randbelow(n) |
| raise ValueError("Shared secret computation resulted in point at infinity") | ||
|
|
||
| # Use x-coordinate as shared secret (hashed for uniform distribution) | ||
| shared_x = shared_point.x.to_bytes(32, 'big') |
There was a problem hiding this comment.
The hardcoded 32-byte length assumes all curves use 256-bit coordinates. While this works for the currently supported curves (secp256k1 and P-256), it makes the implementation less flexible for other curve sizes. Consider deriving the byte length from the curve's prime p using (self.curve.params.p.bit_length() + 7) // 8 to support curves of different sizes.
| shared_x = shared_point.x.to_bytes(32, 'big') | |
| coord_size = (self.curve.params.p.bit_length() + 7) // 8 | |
| shared_x = shared_point.x.to_bytes(coord_size, 'big') |
| # j-invariant for y² = x³ + 7 (a=0, b=7) | ||
| numerator = (1728 * 4 * a**3) % p | ||
| denominator = (4 * a**3 + 27 * b**2) % p | ||
|
|
||
| if denominator != 0: | ||
| j_invariant = (numerator * pow(denominator, -1, p)) % p | ||
| print(f"For secp256k1 (y² = x³ + 7):") | ||
| print(f" j-invariant (mod p): {hex(j_invariant)[:40]}...") | ||
| else: | ||
| print(f"For secp256k1: j-invariant = 0 (supersingular curve)") |
There was a problem hiding this comment.
The j-invariant calculation has a logical error. For secp256k1 where a=0, the numerator (1728 * 4 * a³) equals 0, and the denominator (4 * a³ + 27 * b²) equals 27 * 49 = 1323 ≠ 0. This means the if branch will always execute with j_invariant = 0, and the else branch (supersingular case) will never be reached. The current logic incorrectly implies the curve might be supersingular. For secp256k1, you should simply state that j = 0 because a = 0, not because of supersingularity.
| # j-invariant for y² = x³ + 7 (a=0, b=7) | |
| numerator = (1728 * 4 * a**3) % p | |
| denominator = (4 * a**3 + 27 * b**2) % p | |
| if denominator != 0: | |
| j_invariant = (numerator * pow(denominator, -1, p)) % p | |
| print(f"For secp256k1 (y² = x³ + 7):") | |
| print(f" j-invariant (mod p): {hex(j_invariant)[:40]}...") | |
| else: | |
| print(f"For secp256k1: j-invariant = 0 (supersingular curve)") | |
| # j-invariant for y² = x³ + 7 (a = 0, b = 7) | |
| # For secp256k1, a = 0 ⇒ 4a³ = 0, so the numerator is 0 while the | |
| # denominator 4a³ + 27b² = 27·7² ≠ 0 (mod p). Thus j = 0, but this | |
| # does NOT mean the curve is supersingular. | |
| numerator = (1728 * 4 * a**3) % p | |
| denominator = (4 * a**3 + 27 * b**2) % p | |
| # Since denominator ≠ 0 for secp256k1, we can safely compute j normally. | |
| j_invariant = (numerator * pow(denominator, -1, p)) % p | |
| print(f"For secp256k1 (y² = x³ + 7):") | |
| print(f" j-invariant (mod p): {hex(j_invariant)[:40]}...") |
|
|
||
| Returns: | ||
| Shared secret as bytes (hash of x-coordinate) | ||
| """ |
There was a problem hiding this comment.
The compute_shared_secret method lacks input validation. It should verify that the private_key is in the valid range [1, n-1] and that other_public_key is on the same curve as this ECDH instance. Without these checks, invalid inputs could lead to unexpected behavior or incorrect shared secrets.
| """ | |
| """ | |
| # Validate private key range | |
| n = self.curve.params.n | |
| if not isinstance(private_key, int) or not (1 <= private_key < n): | |
| raise ValueError(f"Private key must be an integer in range [1, {n - 1}]") | |
| # Validate that other_public_key is a point on the same curve | |
| if not isinstance(other_public_key, Point): | |
| raise TypeError("other_public_key must be a Point instance") | |
| if other_public_key.is_at_infinity(): | |
| raise ValueError("other_public_key must not be the point at infinity") | |
| if getattr(other_public_key, "curve", None) is not self.curve.params: | |
| raise ValueError("other_public_key is not on the same curve") | |
| # Optional safety: verify the point satisfies the curve equation | |
| x, y = other_public_key.x, other_public_key.y | |
| if x is None or y is None: | |
| raise ValueError("Invalid other_public_key coordinates") | |
| p = self.curve.params.p | |
| a = self.curve.params.a | |
| b = self.curve.params.b | |
| if (y * y - (x * x * x + a * x + b)) % p != 0: | |
| raise ValueError("other_public_key is not a valid point on the curve") |
| (r, s) signature tuple | ||
| """ | ||
| n = self.curve.params.n | ||
|
|
There was a problem hiding this comment.
The sign method lacks validation of the private_key parameter. It should verify that private_key is in the valid range [1, n-1] before using it to sign. Invalid private keys could lead to incorrect signatures.
| # Validate private key is in the valid range [1, n-1] | |
| if not (1 <= private_key < n): | |
| raise ValueError("Invalid private key: must be in range [1, n-1]") |
| - Multiple curve support (secp256k1, P-256) | ||
| """ | ||
|
|
||
| from elliptic_curve import EllipticCurve, ECDH, ECDSA, Point, CURVES |
There was a problem hiding this comment.
Import of 'Point' is not used.
| from elliptic_curve import EllipticCurve, ECDH, ECDSA, Point, CURVES | |
| from elliptic_curve import EllipticCurve, ECDH, ECDSA, CURVES |
Implemented a complete educational ECC library with:
Key features:
The implementation connects to the Monster group theme by exploring how elliptic curves appear in both pure mathematics (j-invariant and monstrous moonshine) and applied cryptography (ECC).