Skip to content

Commit 795b861

Browse files
gianlucafreitomato42
authored andcommitted
Pk Recovery (#102)
* Added recover_public_keys() to Signature * Added pk recovery support in wrapper
1 parent a7f1db6 commit 795b861

File tree

4 files changed

+108
-0
lines changed

4 files changed

+108
-0
lines changed

src/ecdsa/ecdsa.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,34 @@ def __init__(self, r, s):
6969
self.r = r
7070
self.s = s
7171

72+
def recover_public_keys(self, hash, generator):
73+
"""Returns two public keys for which the signature is valid
74+
hash is signed hash
75+
generator is the used generator of the signature
76+
"""
77+
curve = generator.curve()
78+
n = generator.order()
79+
r = self.r
80+
s = self.s
81+
e = hash
82+
x = r
83+
84+
# Compute the curve point with x as x-coordinate
85+
alpha = (pow(x, 3, curve.p()) + (curve.a() * x) + curve.b()) % curve.p()
86+
beta = numbertheory.square_root_mod_prime(alpha, curve.p())
87+
y = beta if beta % 2 == 0 else curve.p() - beta
88+
89+
# Compute the public key
90+
R1 = ellipticcurve.Point(curve, x, y, n)
91+
Q1 = numbertheory.inverse_mod(r, n) * (s * R1 + (-e % n) * generator)
92+
Pk1 = Public_key(generator, Q1)
93+
94+
# And the second solution
95+
R2 = ellipticcurve.Point(curve, x, -y, n)
96+
Q2 = numbertheory.inverse_mod(r, n) * (s * R2 + (-e % n) * generator)
97+
Pk2 = Public_key(generator, Q2)
98+
99+
return [Pk1, Pk2]
72100

73101
class Public_key(object):
74102
"""Public key for ECDSA.

src/ecdsa/keys.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,30 @@ def from_der(klass, string):
7979
assert point_str.startswith(b("\x00\x04"))
8080
return klass.from_string(point_str[2:], curve)
8181

82+
@classmethod
83+
def from_public_key_recovery(klass, signature, data, curve, hashfunc=sha1, sigdecode=sigdecode_string):
84+
# Given a signature and corresponding message this function
85+
# returns a list of verifying keys for this signature and message
86+
87+
digest = hashfunc(data).digest()
88+
return klass.from_public_key_recovery_with_digest(signature, digest, curve, hashfunc=sha1, sigdecode=sigdecode_string)
89+
90+
@classmethod
91+
def from_public_key_recovery_with_digest(klass, signature, digest, curve, hashfunc=sha1, sigdecode=sigdecode_string):
92+
# Given a signature and corresponding digest this function
93+
# returns a list of verifying keys for this signature and message
94+
95+
generator = curve.generator
96+
r, s = sigdecode(signature, generator.order())
97+
sig = ecdsa.Signature(r, s)
98+
99+
digest_as_number = string_to_number(digest)
100+
pks = sig.recover_public_keys(digest_as_number, generator)
101+
102+
# Transforms the ecdsa.Public_key object into a VerifyingKey
103+
verifying_keys = [klass.from_public_point(pk.point, curve, hashfunc) for pk in pks]
104+
return verifying_keys
105+
82106
def to_string(self):
83107
# VerifyingKey.from_string(vk.to_string()) == vk as long as the
84108
# curves are the same: the curve itself is not included in the

src/ecdsa/test_ecdsa.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,33 @@ def test_signature_validity(Msg, Qx, Qy, R, S, expected):
3030
raise TestFailure("*** Signature test failed: got %s, expected %s." % \
3131
(got, expected))
3232

33+
def test_pk_recovery(Msg, R, S, Qx, Qy):
34+
35+
sign = Signature(R,S)
36+
pks = sign.recover_public_keys(digest_integer(Msg), generator_192)
37+
38+
print_("Test pk recover")
39+
40+
if pks:
41+
42+
# Test if the signature is valid for all found public keys
43+
for pk in pks:
44+
q = pk.point
45+
print_("Recovered q: %s" % q)
46+
test_signature_validity(Msg, q.x(), q.y(), R, S, True)
47+
48+
# Test if the original public key is in the set of found keys
49+
original_q = ellipticcurve.Point(curve_192, Qx, Qy)
50+
points = [pk.point for pk in pks]
51+
if original_q in points:
52+
print_("Original q was found")
53+
else:
54+
raise TestFailure("Original q is not in the list of recovered public keys")
55+
56+
else:
57+
raise TestFailure("*** NO valid public key returned")
58+
59+
3360
print_("NIST Curve P-192:")
3461

3562
p192 = generator_192
@@ -165,13 +192,15 @@ def test_signature_validity(Msg, Qx, Qy, R, S, expected):
165192
R = 0x64dca58a20787c488d11d6dd96313f1b766f2d8efe122916
166193
S = 0x1ecba28141e84ab4ecad92f56720e2cc83eb3d22dec72479
167194
test_signature_validity(Msg, Qx, Qy, R, S, True)
195+
test_pk_recovery(Msg, R, S, Qx, Qy)
168196

169197
Msg = 0x94bb5bacd5f8ea765810024db87f4224ad71362a3c28284b2b9f39fab86db12e8beb94aae899768229be8fdb6c4f12f28912bb604703a79ccff769c1607f5a91450f30ba0460d359d9126cbd6296be6d9c4bb96c0ee74cbb44197c207f6db326ab6f5a659113a9034e54be7b041ced9dcf6458d7fb9cbfb2744d999f7dfd63f4
170198
Qx = 0x3e53ef8d3112af3285c0e74842090712cd324832d4277ae7
171199
Qy = 0xcc75f8952d30aec2cbb719fc6aa9934590b5d0ff5a83adb7
172200
R = 0x8285261607283ba18f335026130bab31840dcfd9c3e555af
173201
S = 0x356d89e1b04541afc9704a45e9c535ce4a50929e33d7e06c
174202
test_signature_validity(Msg, Qx, Qy, R, S, True)
203+
test_pk_recovery(Msg, R, S, Qx, Qy)
175204

176205
Msg = 0xf6227a8eeb34afed1621dcc89a91d72ea212cb2f476839d9b4243c66877911b37b4ad6f4448792a7bbba76c63bdd63414b6facab7dc71c3396a73bd7ee14cdd41a659c61c99b779cecf07bc51ab391aa3252386242b9853ea7da67fd768d303f1b9b513d401565b6f1eb722dfdb96b519fe4f9bd5de67ae131e64b40e78c42dd
177206
Qx = 0x16335dbe95f8e8254a4e04575d736befb258b8657f773cb7

src/ecdsa/test_pyecdsa.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,33 @@ def test_hashfunc(self):
311311
curve=NIST256p)
312312
self.assertTrue(vk3.verify(sig, data, hashfunc=sha256))
313313

314+
def test_public_key_recovery(self):
315+
# Create keys
316+
curve = NIST256p
317+
318+
sk = SigningKey.generate(curve=curve)
319+
vk = sk.get_verifying_key()
320+
321+
# Sign a message
322+
data = b("blahblah")
323+
signature = sk.sign(data)
324+
325+
# Recover verifying keys
326+
recovered_vks = VerifyingKey.from_public_key_recovery(signature, data, curve)
327+
328+
# Test if each pk is valid
329+
for recovered_vk in recovered_vks:
330+
# Test if recovered vk is valid for the data
331+
self.assertTrue(recovered_vk.verify(signature, data))
332+
333+
# Test if properties are equal
334+
self.assertEqual(vk.curve, recovered_vk.curve)
335+
self.assertEqual(vk.default_hashfunc, recovered_vk.default_hashfunc)
336+
337+
# Test if original vk is the list of recovered keys
338+
self.assertTrue(
339+
vk.pubkey.point in [recovered_vk.pubkey.point for recovered_vk in recovered_vks])
340+
314341

315342
class OpenSSL(unittest.TestCase):
316343
# test interoperability with OpenSSL tools. Note that openssl's ECDSA

0 commit comments

Comments
 (0)