Skip to content

Commit 566e22e

Browse files
committed
Fix CVE-2024-23342 by migrating from ecdsa to cryptography library
- Replaced python-ecdsa dependency with cryptography (>=43.0.0) - Rewrote secp256k1_ecdsa.py to use cryptography's EC module - CVE-2024-23342 is a Minerva timing attack in python-ecdsa that affects all versions. The maintainers stated they won't fix it as it's inherent to pure Python implementations - The cryptography library provides constant-time implementations that eliminate this vulnerability - All existing tests pass with the new implementation - Bumped version to 0.12.0 (breaking change due to dependency change) - Updated mypy.ini to remove ecdsa configuration
1 parent 8df07e3 commit 566e22e

File tree

5 files changed

+202
-47
lines changed

5 files changed

+202
-47
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
All notable changes to the Aptos Python SDK will be captured in this file. This changelog is written by hand for now.
44

55
## Unreleased
6-
- Update dependencies for vulnerability fixes
6+
7+
## 0.12.0
8+
9+
- **[Breaking Change]**: Migrated from `ecdsa` to `cryptography` library to fix CVE-2024-23342 (Minerva timing attack vulnerability)
10+
- The `ecdsa` library maintainers stated they will not fix this vulnerability as it's inherent to pure Python implementations
11+
- SECP256K1 ECDSA operations now use the `cryptography` library which provides constant-time implementations
12+
- This change is transparent to users of the SDK - all APIs remain the same
713

814
## 0.11.0
915

aptos_sdk/secp256k1_ecdsa.py

Lines changed: 88 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
import unittest
88
from typing import cast
99

10-
from ecdsa import SECP256k1, SigningKey, VerifyingKey, util
10+
from cryptography.hazmat.primitives.asymmetric import ec
11+
from cryptography.hazmat.primitives import hashes, serialization
12+
from cryptography.hazmat.primitives.asymmetric.utils import (
13+
decode_dss_signature,
14+
encode_dss_signature,
15+
)
1116

1217
from . import asymmetric_crypto
1318
from .bcs import Deserializer, Serializer
@@ -16,19 +21,33 @@
1621
class PrivateKey(asymmetric_crypto.PrivateKey):
1722
LENGTH: int = 32
1823

19-
key: SigningKey
24+
key: ec.EllipticCurvePrivateKey
2025

21-
def __init__(self, key: SigningKey):
26+
def __init__(self, key: ec.EllipticCurvePrivateKey):
2227
self.key = key
2328

2429
def __eq__(self, other: object):
2530
if not isinstance(other, PrivateKey):
2631
return NotImplemented
27-
return self.key == other.key
32+
# Compare private values
33+
return self._to_bytes() == other._to_bytes()
2834

2935
def __str__(self):
3036
return self.aip80()
3137

38+
def _to_bytes(self) -> bytes:
39+
"""Convert private key to raw 32-byte representation."""
40+
private_numbers = self.key.private_numbers()
41+
return private_numbers.private_value.to_bytes(PrivateKey.LENGTH, byteorder="big")
42+
43+
@staticmethod
44+
def _from_bytes(key_bytes: bytes) -> ec.EllipticCurvePrivateKey:
45+
"""Create private key from raw 32-byte representation."""
46+
if len(key_bytes) != PrivateKey.LENGTH:
47+
raise Exception("Length mismatch")
48+
private_value = int.from_bytes(key_bytes, byteorder="big")
49+
return ec.derive_private_key(private_value, ec.SECP256K1())
50+
3251
@staticmethod
3352
def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey:
3453
"""
@@ -43,9 +62,7 @@ def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey:
4362
)
4463
if len(parsed_value.hex()) != PrivateKey.LENGTH * 2:
4564
raise Exception("Length mismatch")
46-
return PrivateKey(
47-
SigningKey.from_string(parsed_value, SECP256k1, hashlib.sha3_256)
48-
)
65+
return PrivateKey(PrivateKey._from_bytes(parsed_value))
4966

5067
@staticmethod
5168
def from_str(value: str, strict: bool | None = None) -> PrivateKey:
@@ -59,61 +76,95 @@ def from_str(value: str, strict: bool | None = None) -> PrivateKey:
5976
return PrivateKey.from_hex(value, strict)
6077

6178
def hex(self) -> str:
62-
return f"0x{self.key.to_string().hex()}"
79+
return f"0x{self._to_bytes().hex()}"
6380

6481
def aip80(self) -> str:
6582
return PrivateKey.format_private_key(
6683
self.hex(), asymmetric_crypto.PrivateKeyVariant.Secp256k1
6784
)
6885

6986
def public_key(self) -> PublicKey:
70-
return PublicKey(self.key.verifying_key)
87+
return PublicKey(self.key.public_key())
7188

7289
@staticmethod
7390
def random() -> PrivateKey:
74-
return PrivateKey(
75-
SigningKey.generate(curve=SECP256k1, hashfunc=hashlib.sha3_256)
76-
)
91+
return PrivateKey(ec.generate_private_key(ec.SECP256K1()))
7792

7893
def sign(self, data: bytes) -> Signature:
79-
sig = self.key.sign_deterministic(data, hashfunc=hashlib.sha3_256)
80-
n = SECP256k1.generator.order()
81-
r, s = util.sigdecode_string(sig, n)
94+
# Use deterministic ECDSA (RFC 6979) with SHA3-256
95+
signature_der = self.key.sign(
96+
data, ec.ECDSA(hashes.SHA3_256())
97+
)
98+
# Decode DER signature to get r and s
99+
r, s = decode_dss_signature(signature_der)
100+
101+
# SECP256K1 curve order
102+
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
103+
82104
# The signature is valid for both s and -s, normalization ensures that only s < n // 2 is valid
83105
if s > (n // 2):
84-
mod_s = (s * -1) % n
85-
sig = util.sigencode_string(r, mod_s, n)
86-
return Signature(sig)
106+
s = n - s
107+
108+
# Encode r and s as raw bytes (32 bytes each)
109+
sig_bytes = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big")
110+
return Signature(sig_bytes)
87111

88112
@staticmethod
89113
def deserialize(deserializer: Deserializer) -> PrivateKey:
90114
key = deserializer.to_bytes()
91115
if len(key) != PrivateKey.LENGTH:
92116
raise Exception("Length mismatch")
93117

94-
return PrivateKey(SigningKey.from_string(key, SECP256k1, hashlib.sha3_256))
118+
return PrivateKey(PrivateKey._from_bytes(key))
95119

96120
def serialize(self, serializer: Serializer):
97-
serializer.to_bytes(self.key.to_string())
121+
serializer.to_bytes(self._to_bytes())
98122

99123

100124
class PublicKey(asymmetric_crypto.PublicKey):
101125
LENGTH: int = 64
102126
LENGTH_WITH_PREFIX_LENGTH: int = 65
103127

104-
key: VerifyingKey
128+
key: ec.EllipticCurvePublicKey
105129

106-
def __init__(self, key: VerifyingKey):
130+
def __init__(self, key: ec.EllipticCurvePublicKey):
107131
self.key = key
108132

109133
def __eq__(self, other: object):
110134
if not isinstance(other, PublicKey):
111135
return NotImplemented
112-
return self.key == other.key
136+
# Compare public key bytes
137+
return self.to_crypto_bytes() == other.to_crypto_bytes()
113138

114139
def __str__(self) -> str:
115140
return self.hex()
116141

142+
@staticmethod
143+
def _from_uncompressed_bytes(key_bytes: bytes) -> ec.EllipticCurvePublicKey:
144+
"""Create public key from uncompressed format (64 or 65 bytes)."""
145+
# Handle optional 0x04 prefix
146+
if len(key_bytes) == PublicKey.LENGTH_WITH_PREFIX_LENGTH:
147+
if key_bytes[0] != 0x04:
148+
raise Exception("Invalid public key format")
149+
key_bytes = key_bytes[1:]
150+
elif len(key_bytes) != PublicKey.LENGTH:
151+
raise Exception("Length mismatch")
152+
153+
# Split into x and y coordinates
154+
x = int.from_bytes(key_bytes[:32], byteorder="big")
155+
y = int.from_bytes(key_bytes[32:], byteorder="big")
156+
157+
# Create public key from numbers
158+
public_numbers = ec.EllipticCurvePublicNumbers(x, y, ec.SECP256K1())
159+
return public_numbers.public_key()
160+
161+
def _to_uncompressed_bytes(self) -> bytes:
162+
"""Convert public key to uncompressed format (64 bytes, no prefix)."""
163+
public_numbers = self.key.public_numbers()
164+
x_bytes = public_numbers.x.to_bytes(32, byteorder="big")
165+
y_bytes = public_numbers.y.to_bytes(32, byteorder="big")
166+
return x_bytes + y_bytes
167+
117168
@staticmethod
118169
def from_str(value: str) -> PublicKey:
119170
if value[0:2] == "0x":
@@ -124,23 +175,30 @@ def from_str(value: str) -> PublicKey:
124175
and len(value) != PublicKey.LENGTH_WITH_PREFIX_LENGTH * 2
125176
):
126177
raise Exception("Length mismatch")
127-
return PublicKey(
128-
VerifyingKey.from_string(bytes.fromhex(value), SECP256k1, hashlib.sha3_256)
129-
)
178+
return PublicKey(PublicKey._from_uncompressed_bytes(bytes.fromhex(value)))
130179

131180
def hex(self) -> str:
132-
return f"0x04{self.key.to_string().hex()}"
181+
return f"0x04{self._to_uncompressed_bytes().hex()}"
133182

134183
def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool:
135184
try:
136185
signature = cast(Signature, signature)
137-
self.key.verify(signature.data(), data)
186+
sig_bytes = signature.data()
187+
188+
# Parse r and s from raw signature bytes (32 bytes each)
189+
r = int.from_bytes(sig_bytes[:32], byteorder="big")
190+
s = int.from_bytes(sig_bytes[32:], byteorder="big")
191+
192+
# Encode as DER for verification
193+
sig_der = encode_dss_signature(r, s)
194+
195+
self.key.verify(sig_der, data, ec.ECDSA(hashes.SHA3_256()))
138196
except Exception:
139197
return False
140198
return True
141199

142200
def to_crypto_bytes(self) -> bytes:
143-
return b"\x04" + self.key.to_string()
201+
return b"\x04" + self._to_uncompressed_bytes()
144202

145203
@staticmethod
146204
def deserialize(deserializer: Deserializer) -> PublicKey:
@@ -152,7 +210,7 @@ def deserialize(deserializer: Deserializer) -> PublicKey:
152210
else:
153211
raise Exception("Length mismatch")
154212

155-
return PublicKey(VerifyingKey.from_string(key, SECP256k1, hashlib.sha3_256))
213+
return PublicKey(PublicKey._from_uncompressed_bytes(key))
156214

157215
def serialize(self, serializer: Serializer):
158216
serializer.to_bytes(self.to_crypto_bytes())

mypy.ini

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
[mypy]
22

3-
[mypy-ecdsa.*]
4-
ignore_missing_imports = True
5-
63
[mypy-python_graphql_client]
74
ignore_missing_imports = True

0 commit comments

Comments
 (0)