Skip to content

Commit d2f2ce3

Browse files
committed
ecdsa key support
1 parent 119a0cb commit d2f2ce3

File tree

8 files changed

+197
-85
lines changed

8 files changed

+197
-85
lines changed
Lines changed: 120 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,112 @@
1-
from cryptography.hazmat.primitives.asymmetric import ed25519
1+
from cryptography.hazmat.primitives.asymmetric import ed25519, ec
22
from cryptography.hazmat.primitives import serialization
3+
from cryptography.hazmat.backends import default_backend
34
from hedera_sdk_python.crypto.public_key import PublicKey
45

6+
57
class PrivateKey:
8+
"""
9+
Represents a private key that can be either Ed25519 or ECDSA (secp256k1).
10+
"""
11+
612
def __init__(self, private_key):
13+
"""
14+
Initializes a PrivateKey from a cryptography PrivateKey object.
15+
"""
716
self._private_key = private_key
817

918
@classmethod
10-
def generate(cls):
19+
def generate(cls, key_type: str = "ed25519"):
1120
"""
12-
Generates a new Ed25519 private key.
21+
Generates a new private key. Defaults to an Ed25519 private key unless
22+
'ecdsa' is specified.
23+
24+
Args:
25+
key_type (str): Either 'ed25519' or 'ecdsa'. Defaults to 'ed25519'.
1326
1427
Returns:
1528
PrivateKey: A new instance of PrivateKey.
1629
"""
17-
private_key = ed25519.Ed25519PrivateKey.generate()
30+
if key_type.lower() == "ed25519":
31+
return cls.generate_ed25519()
32+
elif key_type.lower() == "ecdsa":
33+
return cls.generate_ecdsa()
34+
else:
35+
raise ValueError("Invalid key_type. Use 'ed25519' or 'ecdsa'.")
36+
37+
@classmethod
38+
def generate_ed25519(cls):
39+
"""
40+
Generates a new Ed25519 private key.
41+
42+
Returns:
43+
PrivateKey: A new instance of PrivateKey using Ed25519.
44+
"""
45+
return cls(ed25519.Ed25519PrivateKey.generate())
46+
47+
@classmethod
48+
def generate_ecdsa(cls):
49+
"""
50+
Generates a new ECDSA (secp256k1) private key.
51+
52+
Returns:
53+
PrivateKey: A new instance of PrivateKey using ECDSA.
54+
"""
55+
private_key = ec.generate_private_key(ec.SECP256K1(), default_backend())
1856
return cls(private_key)
1957

2058
@classmethod
2159
def from_string(cls, key_str):
2260
"""
23-
Load a private key from a hex-encoded string. Supports both raw private keys (32 bytes)
24-
and DER-encoded private keys.
61+
Load a private key from a hex-encoded string. For Ed25519, expects 32 bytes.
62+
For ECDSA (secp256k1), also expects 32 bytes (raw scalar).
63+
If it's DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
64+
65+
Args:
66+
key_str (str): The hex-encoded private key string.
67+
68+
Returns:
69+
PrivateKey: A new instance of PrivateKey.
70+
71+
Raises:
72+
ValueError: If the key is invalid or unsupported.
2573
"""
2674
try:
2775
key_bytes = bytes.fromhex(key_str)
2876
except ValueError:
2977
raise ValueError("Invalid hex-encoded private key string.")
3078

79+
if len(key_bytes) == 32:
80+
try:
81+
ed_priv = ed25519.Ed25519PrivateKey.from_private_bytes(key_bytes)
82+
return cls(ed_priv)
83+
except Exception:
84+
pass
85+
try:
86+
private_int = int.from_bytes(key_bytes, "big")
87+
ec_priv = ec.derive_private_key(private_int, ec.SECP256K1(), default_backend())
88+
return cls(ec_priv)
89+
except Exception:
90+
pass
91+
3192
try:
32-
if len(key_bytes) == 32:
33-
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(key_bytes)
34-
else:
35-
private_key = serialization.load_der_private_key(
36-
key_bytes, password=None
37-
)
38-
if not isinstance(private_key, ed25519.Ed25519PrivateKey):
39-
raise TypeError("The key is not an Ed25519 private key.")
40-
return cls(private_key)
93+
private_key = serialization.load_der_private_key(key_bytes, password=None)
4194
except Exception as e:
42-
print(f"Error loading Ed25519 private key: {e}")
43-
raise ValueError("Failed to load private key.")
95+
raise ValueError(f"Failed to load private key (DER): {e}")
96+
97+
if isinstance(private_key, ed25519.Ed25519PrivateKey):
98+
return cls(private_key)
99+
100+
if isinstance(private_key, ec.EllipticCurvePrivateKey):
101+
if not isinstance(private_key.curve, ec.SECP256K1):
102+
raise ValueError("Only secp256k1 ECDSA is supported.")
103+
return cls(private_key)
104+
105+
raise ValueError("Unsupported private key type.")
44106

45-
def sign(self, data):
107+
def sign(self, data: bytes) -> bytes:
46108
"""
47-
Signs the given data using the private key.
109+
Signs the given data using this private key (Ed25519 or ECDSA).
48110
49111
Args:
50112
data (bytes): The data to sign.
@@ -54,40 +116,63 @@ def sign(self, data):
54116
"""
55117
return self._private_key.sign(data)
56118

57-
def public_key(self):
119+
def public_key(self) -> PublicKey:
58120
"""
59-
Retrieves the corresponding public key.
121+
Retrieves the corresponding PublicKey.
60122
61123
Returns:
62124
PublicKey: The public key associated with this private key.
63125
"""
64126
return PublicKey(self._private_key.public_key())
65127

66-
def to_string(self):
128+
def to_bytes_raw(self) -> bytes:
67129
"""
68-
Returns the private key as a hex-encoded string.
130+
Returns the private key bytes in raw form (32 bytes for both Ed25519 and ECDSA).
69131
70132
Returns:
71-
str: The hex-encoded private key.
133+
bytes: The raw private key bytes.
72134
"""
73-
private_bytes = self._private_key.private_bytes(
135+
return self._private_key.private_bytes(
74136
encoding=serialization.Encoding.Raw,
75137
format=serialization.PrivateFormat.Raw,
76138
encryption_algorithm=serialization.NoEncryption()
77139
)
78-
return private_bytes.hex()
79140

141+
def to_string_raw(self) -> str:
142+
"""
143+
Returns the raw private key as a hex-encoded string.
144+
145+
Returns:
146+
str: The hex-encoded raw private key.
147+
"""
148+
return self.to_bytes_raw().hex()
149+
150+
def to_string(self) -> str:
151+
"""
152+
Returns the private key as a hex string (raw).
153+
"""
154+
return self.to_string_raw()
155+
156+
def is_ed25519(self) -> bool:
157+
"""
158+
Checks if this private key is Ed25519.
159+
160+
Returns:
161+
bool: True if Ed25519, False otherwise.
162+
"""
163+
return isinstance(self._private_key, ed25519.Ed25519PrivateKey)
80164

81-
def to_bytes(self):
165+
def is_ecdsa(self) -> bool:
82166
"""
83-
Returns the private key as bytes.
167+
Checks if this private key is ECDSA (secp256k1).
84168
85169
Returns:
86-
bytes: The private key.
170+
bool: True if ECDSA, False otherwise.
87171
"""
88-
private_bytes = self._private_key.private_bytes(
89-
encoding=serialization.Encoding.Raw,
90-
format=serialization.PrivateFormat.Raw,
91-
encryption_algorithm=serialization.NoEncryption()
92-
)
93-
return private_bytes
172+
from cryptography.hazmat.primitives.asymmetric import ec
173+
return isinstance(self._private_key, ec.EllipticCurvePrivateKey)
174+
175+
def __repr__(self):
176+
if self.is_ed25519():
177+
return f"<PrivateKey (Ed25519) hex={self.to_string_raw()}>"
178+
return f"<PrivateKey (ECDSA) hex={self.to_string_raw()}>"
Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
from cryptography.hazmat.primitives.asymmetric import ed25519
1+
from cryptography.hazmat.primitives.asymmetric import ed25519, ec
22
from cryptography.hazmat.primitives import serialization
33

44
class PublicKey:
5+
"""
6+
Represents a public key that can be either Ed25519 or ECDSA (secp256k1).
7+
"""
8+
59
def __init__(self, public_key):
10+
"""
11+
Initializes a PublicKey from a cryptography PublicKey object.
12+
"""
613
self._public_key = public_key
714

8-
def verify(self, signature, data):
15+
def verify(self, signature: bytes, data: bytes) -> None:
916
"""
10-
Verifies a signature for the given data using the public key.
17+
Verifies a signature for the given data using this public key.
18+
Raises an exception if the signature is invalid.
1119
1220
Args:
1321
signature (bytes): The signature to verify.
@@ -18,42 +26,73 @@ def verify(self, signature, data):
1826
"""
1927
self._public_key.verify(signature, data)
2028

21-
def to_string(self):
29+
def to_bytes_raw(self) -> bytes:
2230
"""
23-
Returns the public key as a hex-encoded string.
31+
Returns the public key in raw form:
32+
- For Ed25519, it's 32 bytes.
33+
- For ECDSA (secp256k1), it's the uncompressed or compressed form,
34+
depending on how cryptography outputs RAW. Usually 33 bytes compressed.
2435
2536
Returns:
26-
str: The hex-encoded public key.
37+
bytes: The raw public key bytes.
2738
"""
28-
public_bytes = self._public_key.public_bytes(
39+
return self._public_key.public_bytes(
2940
encoding=serialization.Encoding.Raw,
3041
format=serialization.PublicFormat.Raw
3142
)
32-
return public_bytes.hex()
3343

34-
def to_proto(self):
44+
def to_string_raw(self) -> str:
3545
"""
36-
Returns the protobuf representation of the public key.
46+
Returns the raw public key as a hex-encoded string.
3747
3848
Returns:
39-
Key: The protobuf Key message.
49+
str: The hex-encoded raw public key.
4050
"""
41-
from hedera_sdk_python.hapi.services import basic_types_pb2
42-
public_bytes = self._public_key.public_bytes(
43-
encoding=serialization.Encoding.Raw,
44-
format=serialization.PublicFormat.Raw
45-
)
46-
return basic_types_pb2.Key(ed25519=public_bytes)
51+
return self.to_bytes_raw().hex()
52+
53+
def to_string(self) -> str:
54+
"""
55+
Returns the private key as a hex string (raw).
56+
"""
57+
return self.to_string_raw()
4758

48-
def public_bytes(self, encoding, format):
59+
60+
def is_ed25519(self) -> bool:
4961
"""
50-
Returns the public key bytes in the specified encoding and format.
62+
Checks if this public key is Ed25519.
5163
52-
Args:
53-
encoding (Encoding): The encoding to use.
54-
format (PublicFormat): The public key format.
64+
Returns:
65+
bool: True if Ed25519, False otherwise.
66+
"""
67+
return isinstance(self._public_key, ed25519.Ed25519PublicKey)
68+
69+
def is_ecdsa(self) -> bool:
70+
"""
71+
Checks if this public key is ECDSA (secp256k1).
72+
73+
Returns:
74+
bool: True if ECDSA, False otherwise.
75+
"""
76+
return isinstance(self._public_key, ec.EllipticCurvePublicKey)
77+
78+
def to_proto(self):
79+
"""
80+
Returns the protobuf representation of the public key.
81+
For Ed25519, uses the 'ed25519' field in Key.
82+
For ECDSA, uses the 'ECDSASecp256k1' field (may differ by your actual Hedera environment).
5583
5684
Returns:
57-
bytes: The public key bytes.
85+
Key: The protobuf Key message.
5886
"""
59-
return self._public_key.public_bytes(encoding=encoding, format=format)
87+
from hedera_sdk_python.hapi.services import basic_types_pb2
88+
89+
pub_bytes = self.to_bytes_raw()
90+
if self.is_ed25519():
91+
return basic_types_pb2.Key(ed25519=pub_bytes)
92+
else:
93+
return basic_types_pb2.Key(ECDSASecp256k1=pub_bytes)
94+
95+
def __repr__(self):
96+
if self.is_ed25519():
97+
return f"<PublicKey (Ed25519) hex={self.to_string_raw()}>"
98+
return f"<PublicKey (ECDSA) hex={self.to_string_raw()}>"

src/hedera_sdk_python/tokens/token_create_transaction.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,7 @@ def build_transaction_body(self):
8888

8989
admin_key_proto = None
9090
if self.admin_key:
91-
admin_public_key_bytes = self.admin_key.public_key().public_bytes(
92-
encoding=serialization.Encoding.Raw,
93-
format=serialization.PublicFormat.Raw
94-
)
91+
admin_public_key_bytes = self.admin_key.public_key().to_bytes_raw()
9592
admin_key_proto = basic_types_pb2.Key(ed25519=admin_public_key_bytes)
9693

9794
token_create_body = token_create_pb2.TokenCreateTransactionBody(

src/hedera_sdk_python/transaction/transaction.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ def sign(self, private_key):
4747

4848
signature = private_key.sign(self.transaction_body_bytes)
4949

50-
public_key_bytes = private_key.public_key().public_bytes(
51-
encoding=Encoding.Raw,
52-
format=PublicFormat.Raw
53-
)
50+
public_key_bytes = private_key.public_key().to_bytes_raw()
5451

5552
sig_pair = basic_types_pb2.SignaturePair(
5653
pubKeyPrefix=public_key_bytes,
@@ -149,10 +146,7 @@ def is_signed_by(self, public_key):
149146
Returns:
150147
bool: True if signed by the given public key, False otherwise.
151148
"""
152-
public_key_bytes = public_key.public_bytes(
153-
encoding=Encoding.Raw,
154-
format=PublicFormat.Raw
155-
)
149+
public_key_bytes = public_key.to_bytes_raw()
156150

157151
for sig_pair in self.signature_map.sigPair:
158152
if sig_pair.pubKeyPrefix == public_key_bytes:

tests/test_account_create_transaction.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,7 @@ def test_account_create_transaction_build(mock_account_ids):
4343

4444
transaction_body = account_tx.build_transaction_body()
4545

46-
expected_public_key_bytes = new_public_key.public_bytes(
47-
encoding=serialization.Encoding.Raw,
48-
format=serialization.PublicFormat.Raw
49-
)
46+
expected_public_key_bytes = new_public_key.to_bytes_raw()
5047

5148
assert transaction_body.cryptoCreateAccount.key.ed25519 == expected_public_key_bytes
5249
assert transaction_body.cryptoCreateAccount.initialBalance == 100000000

tests/test_token_associate_transaction.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_sign_transaction(mock_account_ids):
5858

5959
private_key = MagicMock()
6060
private_key.sign.return_value = b'signature'
61-
private_key.public_key().public_bytes.return_value = b'public_key'
61+
private_key.public_key().to_bytes_raw.return_value = b'public_key'
6262

6363
associate_tx.sign(private_key)
6464

@@ -80,7 +80,7 @@ def test_to_proto(mock_account_ids):
8080

8181
private_key = MagicMock()
8282
private_key.sign.return_value = b'signature'
83-
private_key.public_key().public_bytes.return_value = b'public_key'
83+
private_key.public_key().to_bytes_raw.return_value = b'public_key'
8484

8585
associate_tx.sign(private_key)
8686
proto = associate_tx.to_proto()

0 commit comments

Comments
 (0)