Skip to content

Commit cd06bbb

Browse files
authored
Merge pull request #29 from daedalus/copilot/improve-slow-code-performance
[WIP] Identify and suggest improvements for slow code
2 parents caa14c9 + a4a4620 commit cd06bbb

File tree

2 files changed

+179
-75
lines changed

2 files changed

+179
-75
lines changed

crack_weak_ECDSA_nonces_with_LLL.py

Lines changed: 178 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#!/usr/bin/env python
2-
# Author: Dario Clavijo (2020)
3-
# MIT License
2+
# Author: Dario Clavijo (2020)
3+
# MIT License
44

55
import sys
66
import argparse
77
import mmap
8+
import heapq
89
import gmpy2
910
from fpylll import IntegerMatrix, LLL, BKZ
1011
from ecdsa import SigningKey, SECP256k1
11-
from ecdsa.ecdsa import int_to_string
1212

1313
DEFAULT_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
1414

@@ -29,6 +29,7 @@ def normalize_pubhex(s: str) -> str:
2929

3030
def load_csv(filename, limit=None, mmap_flag=False):
3131
msgs, sigs, pubs = [], [], []
32+
3233
def parse_pub(p):
3334
return normalize_pubhex(p)
3435

@@ -39,10 +40,10 @@ def parse_pub(p):
3940
for n, line in enumerate(lines):
4041
if limit is not None and n >= limit:
4142
break
42-
l = line.decode("utf-8").rstrip().split(",")
43-
if len(l) < 5:
43+
parts = line.decode("utf-8").rstrip().split(",")
44+
if len(parts) < 5:
4445
continue
45-
tx, R, S, Z, pub = l[:5]
46+
tx, R, S, Z, pub = parts[:5]
4647
msgs.append(int(Z, 16))
4748
sigs.append((int(R, 16), int(S, 16)))
4849
pubs.append(parse_pub(pub))
@@ -127,11 +128,64 @@ def point_bytes_from_vk(vk):
127128
return uncompressed.lower(), compressed.lower()
128129

129130

130-
def privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, order, max_rows=20, max_candidates=1000):
131+
def _compute_row_norms(matrix, m, max_rows):
132+
"""Compute norms for matrix rows, returning up to max_rows smallest.
133+
134+
Args:
135+
matrix: The reduced lattice matrix
136+
m: Number of columns to include in norm computation
137+
max_rows: Maximum number of rows to return
138+
139+
Returns:
140+
List of (norm, row_index) tuples, sorted by norm (smallest first).
141+
Returns min(matrix.nrows, max_rows) items.
142+
143+
Uses heapq.nsmallest for efficient partial sorting when only a subset
144+
of rows is needed. Falls back to full sort when all rows are needed.
145+
"""
146+
norms_generator = (
147+
(sum(float(matrix[ridx, j]) ** 2 for j in range(m)), ridx)
148+
for ridx in range(matrix.nrows)
149+
)
150+
151+
if matrix.nrows <= max_rows:
152+
# Need all rows (fewer than max_rows exist) - full sort is fine
153+
return sorted(norms_generator)
154+
else:
155+
# Need only top max_rows out of many - heapq is more efficient
156+
return heapq.nsmallest(max_rows, norms_generator)
157+
158+
159+
def _extract_candidate_from_row(row_val, b, cd, ab_list, order):
160+
"""Extract private key candidates from a single row value."""
161+
candidates = []
162+
base = (cd - (b * row_val)) % order
163+
164+
if ab_list is None:
165+
candidate = base % order
166+
if 1 <= candidate < order:
167+
candidates.append(candidate)
168+
else:
169+
for ab in ab_list:
170+
if ab:
171+
try:
172+
inv = modular_inv(ab, order)
173+
candidate = (base * inv) % order
174+
if 1 <= candidate < order:
175+
candidates.append(candidate)
176+
except Exception:
177+
pass
178+
return candidates
179+
180+
181+
def privkeys_from_reduced_matrix(
182+
msgs, sigs, pubs, matrix, order, max_rows=20, max_candidates=1000
183+
):
131184
keys = set()
132185
m = len(msgs)
133186
msgn, rn, sn = msgs[-1], sigs[-1][0], sigs[-1][1]
134187

188+
# Precompute parameters for each signature
135189
params = []
136190
for i in range(m):
137191
a = (rn * sigs[i][1]) % order
@@ -142,76 +196,74 @@ def privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, order, max_rows=20, m
142196
ab_list = None if a == b else [((a - b) % order), ((b - a) % order)]
143197
params.append((b, cd, ab_list))
144198

145-
# compute row norms (only first m columns contribute to norm in our design)
146-
row_norms = []
147-
for ridx in range(matrix.nrows):
148-
# norm over first m columns
149-
norm2 = 0.0
150-
for j in range(m):
151-
v = float(matrix[ridx, j])
152-
norm2 += v * v
153-
row_norms.append((norm2, ridx))
154-
row_norms.sort()
155-
156-
checked = 0
157-
for _, ridx in row_norms[:max_rows]:
158-
if checked >= max_candidates:
199+
# Get sorted row indices by norm
200+
row_norms = _compute_row_norms(matrix, m, max_rows)
201+
202+
# Extract candidates from best rows
203+
for _, ridx in row_norms:
204+
if len(keys) >= max_candidates:
159205
break
160206
row = [int(matrix[ridx, j]) for j in range(m)]
161207
for i, (b, cd, ab_list) in enumerate(params):
162-
base = (cd - (b * row[i])) % order
163-
if ab_list is None:
164-
# direct candidate
165-
candidate = base % order
166-
if 1 <= candidate < order:
167-
keys.add(candidate)
168-
else:
169-
for ab in ab_list:
170-
if ab:
171-
try:
172-
inv = modular_inv(ab, order)
173-
except Exception:
174-
continue
175-
candidate = (base * inv) % order
176-
if 1 <= candidate < order:
177-
keys.add(candidate)
178-
checked = len(keys)
179-
if checked >= max_candidates:
180-
break
208+
candidates = _extract_candidate_from_row(row[i], b, cd, ab_list, order)
209+
keys.update(candidates)
210+
if len(keys) >= max_candidates:
211+
break
181212

182213
return list(keys)
183214

184215

185-
def derived_pubhexes_for_candidates(candidates):
186-
"""Given a list of private integer candidates, derive both compressed and uncompressed pub hexes."""
187-
mapping = {}
188-
for priv in candidates:
189-
try:
190-
sk = SigningKey.from_secret_exponent(priv, curve=SECP256k1)
191-
vk = sk.get_verifying_key()
192-
uncmp, cmpd = point_bytes_from_vk(vk)
193-
mapping[priv] = (uncmp, cmpd)
194-
except Exception:
195-
# skip invalid privs
196-
continue
197-
return mapping
216+
def _try_derive_pubkey(priv):
217+
"""Attempt to derive public key from private key."""
218+
try:
219+
sk = SigningKey.from_secret_exponent(priv, curve=SECP256k1)
220+
vk = sk.get_verifying_key()
221+
return point_bytes_from_vk(vk)
222+
except Exception:
223+
return None, None
224+
225+
226+
def _verify_candidates(keys, pubset, find_all=False):
227+
"""Verify candidate private keys against known public keys.
228+
229+
Args:
230+
keys: List of candidate private keys to verify
231+
pubset: Set of known public keys (hex strings)
232+
find_all: If True, find all matching keys. If False, stop after
233+
first match (optimization for typical use case)
234+
235+
Returns:
236+
List of tuples (priv, uncompressed_pub, compressed_pub)
237+
"""
238+
verified = []
239+
for priv in keys:
240+
uncmp, cmpd = _try_derive_pubkey(priv)
241+
if uncmp and (uncmp in pubset or cmpd in pubset):
242+
verified.append((priv, uncmp, cmpd))
243+
if not find_all:
244+
break # Early exit optimization after first match
245+
return verified
198246

199247

200248
def display_keys(keys, pubkeys, show_all=False):
249+
"""Display verified private keys or all candidates.
250+
251+
Args:
252+
keys: List of candidate private keys
253+
pubkeys: List of known public keys for verification
254+
show_all: If True, show all candidates when none verify. Also finds
255+
all matching keys instead of stopping at first match.
256+
"""
201257
pubset = set(normalize_pubhex(p) for p in pubkeys if p)
202258
if not pubset:
203259
sys.stderr.write("[!] Warning: no pubkeys given for verification.\n")
204260

205-
# batch derive pubkeys
206-
mapping = derived_pubhexes_for_candidates(keys)
207261
verified = []
208-
for priv, (uncmp, cmpd) in mapping.items():
209-
if uncmp in pubset or cmpd in pubset:
210-
verified.append((priv, uncmp, cmpd))
262+
if pubset:
263+
verified = _verify_candidates(keys, pubset, find_all=show_all)
211264

212265
if not verified:
213266
if show_all:
214-
# show all candidates in hex
215267
print("Recovered candidates (not verified):")
216268
for k in keys:
217269
print(f"{k:064x}")
@@ -227,30 +279,83 @@ def display_keys(keys, pubkeys, show_all=False):
227279

228280

229281
def main():
230-
parser = argparse.ArgumentParser(description="ECDSA private key recovery using lattice reduction (fpylll)")
282+
parser = argparse.ArgumentParser(
283+
description="ECDSA private key recovery using lattice reduction"
284+
)
231285
parser.add_argument("filename", help="CSV file containing ECDSA traces")
232286
parser.add_argument("B", type=int, help="log2 bound parameter B")
233-
parser.add_argument("limit", type=int, nargs="?", default=None, help="Limit number of signatures to process (optional)")
234-
parser.add_argument("--order", type=int, default=DEFAULT_ORDER, help="Curve order (default: secp256k1)")
235-
parser.add_argument("--reduction", choices=["LLL", "BKZ"], default="LLL", help="Lattice reduction algorithm")
236-
parser.add_argument("--mmap", action="store_true", help="Enable mmap for fast CSV access")
237-
parser.add_argument("--integer_mode", action="store_true", help="Scale matrix to ensure integer values")
238-
parser.add_argument("--max_rows", type=int, default=20, help="Max number of reduced rows to inspect")
239-
parser.add_argument("--max_candidates", type=int, default=1000, help="Stop after this many candidates are gathered")
240-
parser.add_argument("--show_all", action="store_true", help="Show all recovered candidates even if not verified")
287+
parser.add_argument(
288+
"limit",
289+
type=int,
290+
nargs="?",
291+
default=None,
292+
help="Limit number of signatures to process (optional)",
293+
)
294+
parser.add_argument(
295+
"--order",
296+
type=int,
297+
default=DEFAULT_ORDER,
298+
help="Curve order (default: secp256k1)",
299+
)
300+
parser.add_argument(
301+
"--reduction",
302+
choices=["LLL", "BKZ"],
303+
default="LLL",
304+
help="Lattice reduction algorithm",
305+
)
306+
parser.add_argument(
307+
"--mmap", action="store_true", help="Enable mmap for fast CSV access"
308+
)
309+
parser.add_argument(
310+
"--integer_mode",
311+
action="store_true",
312+
help="Scale matrix to ensure integer values",
313+
)
314+
parser.add_argument(
315+
"--max_rows",
316+
type=int,
317+
default=20,
318+
help="Max number of reduced rows to inspect",
319+
)
320+
parser.add_argument(
321+
"--max_candidates",
322+
type=int,
323+
default=1000,
324+
help="Stop after this many candidates are gathered",
325+
)
326+
parser.add_argument(
327+
"--show_all",
328+
action="store_true",
329+
help="Show all recovered candidates even if not verified",
330+
)
241331
args = parser.parse_args()
242332

243-
msgs, sigs, pubs = load_csv(args.filename, limit=args.limit, mmap_flag=args.mmap)
333+
msgs, sigs, pubs = load_csv(
334+
args.filename, limit=args.limit, mmap_flag=args.mmap
335+
)
244336
sys.stderr.write(f"Using: {len(msgs)} sigs...\n")
245337
if len(msgs) < 2:
246-
sys.stderr.write("[!] Warning: fewer than 2 signatures - results may be meaningless.\n")
247-
248-
matrix = make_matrix_fpylll(msgs, sigs, args.B, args.order, integer_mode=args.integer_mode)
338+
sys.stderr.write(
339+
"[!] Warning: fewer than 2 signatures - "
340+
"results may be meaningless.\n"
341+
)
342+
343+
matrix = make_matrix_fpylll(
344+
msgs, sigs, args.B, args.order, integer_mode=args.integer_mode
345+
)
249346
sys.stderr.write("Matrix constructed, starting reduction...\n")
250347
matrix = reduce_matrix(matrix, algorithm=args.reduction)
251348
sys.stderr.write("Reduction complete, extracting candidates...\n")
252349

253-
keys = privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, args.order, max_rows=args.max_rows, max_candidates=args.max_candidates)
350+
keys = privkeys_from_reduced_matrix(
351+
msgs,
352+
sigs,
353+
pubs,
354+
matrix,
355+
args.order,
356+
max_rows=args.max_rows,
357+
max_candidates=args.max_candidates,
358+
)
254359
sys.stderr.write(f"Candidates found: {len(keys)}\n")
255360

256361
display_keys(keys, pubs, show_all=args.show_all)

weak_signature_generator.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@
3030

3131

3232
def inttohex(i):
33-
tmpstr = hex(i)
34-
return tmpstr.replace("0x", "").replace("L", "").zfill(64)
33+
return format(i, '064x')
3534

3635

3736
for i in range(0, len(msgs)):

0 commit comments

Comments
 (0)