1
1
import boto3
2
- from asn1crypto import core
2
+ from cryptography .hazmat .primitives import serialization
3
+ from cryptography .hazmat .primitives .asymmetric .utils import decode_dss_signature
3
4
from eth_account .messages import encode_typed_data , _hash_eip191_message
5
+ from eth_keys .backends .native .ecdsa import N as SECP256K1_N
4
6
from eth_keys .datatypes import Signature
5
7
from eth_utils import keccak , to_hex
6
8
from hyperliquid .exchange import Exchange
7
9
from hyperliquid .utils .constants import TESTNET_API_URL , MAINNET_API_URL
8
10
from hyperliquid .utils .signing import get_timestamp_ms , action_hash , construct_phantom_agent , l1_payload
9
11
from loguru import logger
10
12
13
+ SECP256K1_N_HALF = SECP256K1_N // 2
14
+
11
15
12
16
class KMSSigner :
13
- def __init__ (self , key_id , aws_region_name , use_testnet ):
17
+ def __init__ (self , config ):
18
+ use_testnet = config ["hyperliquid" ]["use_testnet" ]
14
19
url = TESTNET_API_URL if use_testnet else MAINNET_API_URL
15
20
self .oracle_publisher_exchange : Exchange = Exchange (wallet = None , base_url = url )
21
+ self .client = self ._init_client (config )
16
22
17
- self .key_id = key_id
18
- self .client = boto3 .client ("kms" , region_name = aws_region_name )
19
23
# Fetch public key once so we can derive address and check recovery id
20
- pub_der = self .client .get_public_key (KeyId = key_id )["PublicKey" ]
21
-
22
- from cryptography .hazmat .primitives import serialization
23
- pub = serialization .load_der_public_key (pub_der )
24
+ key_path = config ["kms" ]["key_path" ]
25
+ self .key_id = open (key_path , "r" ).read ().strip ()
26
+ self .pubkey_der = self .client .get_public_key (KeyId = self .key_id )["PublicKey" ]
27
+ # Construct eth address to log
28
+ pub = serialization .load_der_public_key (self .pubkey_der )
24
29
numbers = pub .public_numbers ()
25
30
x = numbers .x .to_bytes (32 , "big" )
26
31
y = numbers .y .to_bytes (32 , "big" )
@@ -29,6 +34,22 @@ def __init__(self, key_id, aws_region_name, use_testnet):
29
34
self .address = "0x" + keccak (uncompressed [1 :])[- 20 :].hex ()
30
35
logger .info ("KMSSigner address: {}" , self .address )
31
36
37
+ def _init_client (self , config ):
38
+ aws_region_name = config ["kms" ]["aws_region_name" ]
39
+ access_key_id_path = config ["kms" ]["access_key_id_path" ]
40
+ access_key_id = open (access_key_id_path , "r" ).read ().strip ()
41
+ secret_access_key_path = config ["kms" ]["secret_access_key_path" ]
42
+ secret_access_key = open (secret_access_key_path , "r" ).read ().strip ()
43
+
44
+ return boto3 .client (
45
+ "kms" ,
46
+ region_name = aws_region_name ,
47
+ aws_access_key_id = access_key_id ,
48
+ aws_secret_access_key = secret_access_key ,
49
+ # can specify an endpoint for e.g. LocalStack
50
+ # endpoint_url="http://localhost:4566"
51
+ )
52
+
32
53
def set_oracle (self , dex , oracle_pxs , all_mark_pxs , external_perp_pxs ):
33
54
timestamp = get_timestamp_ms ()
34
55
oracle_pxs_wire = sorted (list (oracle_pxs .items ()))
@@ -60,34 +81,39 @@ def sign_l1_action(self, action, nonce, is_mainnet):
60
81
data = l1_payload (phantom_agent )
61
82
structured_data = encode_typed_data (full_message = data )
62
83
message_hash = _hash_eip191_message (structured_data )
63
- signed = self .sign_message (message_hash )
64
- return {"r" : to_hex (signed ["r" ]), "s" : to_hex (signed ["s" ]), "v" : signed ["v" ]}
84
+ return self .sign_message (message_hash )
65
85
66
- def sign_message (self , message_hash : bytes ):
86
+ def sign_message (self , message_hash : bytes ) -> dict :
87
+ # Send message hash to KMS for signing
67
88
resp = self .client .sign (
68
89
KeyId = self .key_id ,
69
90
Message = message_hash ,
70
91
MessageType = "DIGEST" ,
71
92
SigningAlgorithm = "ECDSA_SHA_256" , # required for secp256k1
72
93
)
73
- der_sig = resp ["Signature" ]
74
-
75
- seq = core .Sequence .load (der_sig )
76
- r = int (seq [0 ].native )
77
- s = int (seq [1 ].native )
78
-
79
- for recovery_id in (0 , 1 ):
80
- candidate = Signature (vrs = (recovery_id , r , s ))
81
- pubkey = candidate .recover_public_key_from_msg_hash (message_hash )
82
- if pubkey .to_bytes () == self .public_key_bytes :
83
- v = recovery_id + 27
84
- break
85
- else :
86
- raise ValueError ("Failed to determine recovery id" )
87
-
88
- return {
89
- "r" : r ,
90
- "s" : s ,
91
- "v" : v ,
92
- "signature" : Signature (vrs = (v , r , s )).to_bytes ().hex (),
93
- }
94
+ kms_signature = resp ["Signature" ]
95
+ # Decode the KMS DER signature -> (r, s)
96
+ r , s = decode_dss_signature (kms_signature )
97
+ # Ethereum requires low-s form
98
+ if s > SECP256K1_N_HALF :
99
+ s = SECP256K1_N - s
100
+ # Parse KMS public key into uncompressed secp256k1 bytes
101
+ # TODO: Pull this into init
102
+ pubkey = serialization .load_der_public_key (self .pubkey_der )
103
+ pubkey_bytes = pubkey .public_bytes (
104
+ serialization .Encoding .X962 ,
105
+ serialization .PublicFormat .UncompressedPoint ,
106
+ )
107
+ # Strip leading 0x04 (uncompressed point indicator)
108
+ raw_pubkey_bytes = pubkey_bytes [1 :]
109
+ # Try both recovery ids
110
+ for v in (0 , 1 ):
111
+ sig_obj = Signature (vrs = (v , r , s ))
112
+ recovered_pub = sig_obj .recover_public_key_from_msg_hash (message_hash )
113
+ if recovered_pub .to_bytes () == raw_pubkey_bytes :
114
+ return {
115
+ "r" : to_hex (r ),
116
+ "s" : to_hex (s ),
117
+ "v" : v + 27 ,
118
+ }
119
+ raise ValueError ("Could not recover public key; signature mismatch" )
0 commit comments