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
+ from config import Config
14
+
15
+ SECP256K1_N_HALF = SECP256K1_N // 2
16
+
11
17
12
18
class KMSSigner :
13
- def __init__ (self , key_id , aws_region_name , use_testnet ):
19
+ def __init__ (self , config : Config ):
20
+ use_testnet = config .hyperliquid .use_testnet
14
21
url = TESTNET_API_URL if use_testnet else MAINNET_API_URL
15
22
self .oracle_publisher_exchange : Exchange = Exchange (wallet = None , base_url = url )
23
+ self .client = self ._init_client (config )
16
24
17
- self .key_id = key_id
18
- self .client = boto3 .client ("kms" , region_name = aws_region_name )
19
25
# 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 )
26
+ key_path = config .kms .key_path
27
+ self .key_id = open (key_path , "r" ).read ().strip ()
28
+ self .pubkey_der = self .client .get_public_key (KeyId = self .key_id )["PublicKey" ]
29
+ # Construct eth address to log
30
+ pub = serialization .load_der_public_key (self .pubkey_der )
24
31
numbers = pub .public_numbers ()
25
32
x = numbers .x .to_bytes (32 , "big" )
26
33
y = numbers .y .to_bytes (32 , "big" )
@@ -29,6 +36,22 @@ def __init__(self, key_id, aws_region_name, use_testnet):
29
36
self .address = "0x" + keccak (uncompressed [1 :])[- 20 :].hex ()
30
37
logger .info ("KMSSigner address: {}" , self .address )
31
38
39
+ def _init_client (self , config ):
40
+ aws_region_name = config .kms .aws_region_name
41
+ access_key_id_path = config .kms .access_key_id_path
42
+ access_key_id = open (access_key_id_path , "r" ).read ().strip ()
43
+ secret_access_key_path = config .kms .secret_access_key_path
44
+ secret_access_key = open (secret_access_key_path , "r" ).read ().strip ()
45
+
46
+ return boto3 .client (
47
+ "kms" ,
48
+ region_name = aws_region_name ,
49
+ aws_access_key_id = access_key_id ,
50
+ aws_secret_access_key = secret_access_key ,
51
+ # can specify an endpoint for e.g. LocalStack
52
+ # endpoint_url="http://localhost:4566"
53
+ )
54
+
32
55
def set_oracle (self , dex , oracle_pxs , all_mark_pxs , external_perp_pxs ):
33
56
timestamp = get_timestamp_ms ()
34
57
oracle_pxs_wire = sorted (list (oracle_pxs .items ()))
@@ -60,34 +83,39 @@ def sign_l1_action(self, action, nonce, is_mainnet):
60
83
data = l1_payload (phantom_agent )
61
84
structured_data = encode_typed_data (full_message = data )
62
85
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" ]}
86
+ return self .sign_message (message_hash )
65
87
66
- def sign_message (self , message_hash : bytes ):
88
+ def sign_message (self , message_hash : bytes ) -> dict :
89
+ # Send message hash to KMS for signing
67
90
resp = self .client .sign (
68
91
KeyId = self .key_id ,
69
92
Message = message_hash ,
70
93
MessageType = "DIGEST" ,
71
94
SigningAlgorithm = "ECDSA_SHA_256" , # required for secp256k1
72
95
)
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
- }
96
+ kms_signature = resp ["Signature" ]
97
+ # Decode the KMS DER signature -> (r, s)
98
+ r , s = decode_dss_signature (kms_signature )
99
+ # Ethereum requires low-s form
100
+ if s > SECP256K1_N_HALF :
101
+ s = SECP256K1_N - s
102
+ # Parse KMS public key into uncompressed secp256k1 bytes
103
+ # TODO: Pull this into init
104
+ pubkey = serialization .load_der_public_key (self .pubkey_der )
105
+ pubkey_bytes = pubkey .public_bytes (
106
+ serialization .Encoding .X962 ,
107
+ serialization .PublicFormat .UncompressedPoint ,
108
+ )
109
+ # Strip leading 0x04 (uncompressed point indicator)
110
+ raw_pubkey_bytes = pubkey_bytes [1 :]
111
+ # Try both recovery ids
112
+ for v in (0 , 1 ):
113
+ sig_obj = Signature (vrs = (v , r , s ))
114
+ recovered_pub = sig_obj .recover_public_key_from_msg_hash (message_hash )
115
+ if recovered_pub .to_bytes () == raw_pubkey_bytes :
116
+ return {
117
+ "r" : to_hex (r ),
118
+ "s" : to_hex (s ),
119
+ "v" : v + 27 ,
120
+ }
121
+ raise ValueError ("Could not recover public key; signature mismatch" )
0 commit comments