Skip to content

Commit 0bba1e3

Browse files
committed
support for SEC/X9.62 formatted keys
Adds support for encoding and decoding verifying keys in format specified in SEC 1 or in X9.62. Specifically the uncompressed point encoding and the compressed point encoding
1 parent bcf6afe commit 0bba1e3

File tree

3 files changed

+189
-19
lines changed

3 files changed

+189
-19
lines changed

src/ecdsa/keys.py

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from . import ecdsa
44
from . import der
55
from . import rfc6979
6+
from . import ellipticcurve
67
from .curves import NIST192p, find_curve
8+
from .numbertheory import square_root_mod_prime, SquareRootError
79
from .ecdsa import RSZeroError
810
from .util import string_to_number, number_to_string, randrange
911
from .util import sigencode_string, sigdecode_string
@@ -23,6 +25,10 @@ class BadDigestError(Exception):
2325
pass
2426

2527

28+
class MalformedPoint(AssertionError):
29+
pass
30+
31+
2632
class VerifyingKey:
2733
def __init__(self, _error__please_use_generate=None):
2834
if not _error__please_use_generate:
@@ -38,9 +44,8 @@ def from_public_point(klass, point, curve=NIST192p, hashfunc=sha1):
3844
self.pubkey.order = curve.order
3945
return self
4046

41-
@classmethod
42-
def from_string(klass, string, curve=NIST192p, hashfunc=sha1,
43-
validate_point=True):
47+
@staticmethod
48+
def _from_raw_encoding(string, curve, validate_point):
4449
order = curve.order
4550
assert (len(string) == curve.verifying_key_length), \
4651
(len(string), curve.verifying_key_length)
@@ -52,8 +57,49 @@ def from_string(klass, string, curve=NIST192p, hashfunc=sha1,
5257
y = string_to_number(ys)
5358
if validate_point:
5459
assert ecdsa.point_is_valid(curve.generator, x, y)
55-
from . import ellipticcurve
56-
point = ellipticcurve.Point(curve.curve, x, y, order)
60+
return ellipticcurve.Point(curve.curve, x, y, order)
61+
62+
@staticmethod
63+
def _from_compressed(string, curve, validate_point):
64+
if string[:1] not in (b('\x02'), b('\x03')):
65+
raise MalformedPoint("Malformed compressed point encoding")
66+
67+
is_even = string[:1] == b('\x02')
68+
x = string_to_number(string[1:])
69+
order = curve.order
70+
p = curve.curve.p()
71+
alpha = (pow(x, 3, p) + (curve.curve.a() * x) + curve.curve.b()) % p
72+
try:
73+
beta = square_root_mod_prime(alpha, p)
74+
except SquareRootError as e:
75+
raise MalformedPoint("Encoding does not correspond to a point on "
76+
"curve", e)
77+
if is_even == bool(beta & 1):
78+
y = p - beta
79+
else:
80+
y = beta
81+
if validate_point and not ecdsa.point_is_valid(curve.generator, x, y):
82+
raise MalformedPoint("Point does not lie on curve")
83+
return ellipticcurve.Point(curve.curve, x, y, order)
84+
85+
@classmethod
86+
def from_string(klass, string, curve=NIST192p, hashfunc=sha1,
87+
validate_point=True):
88+
sig_len = len(string)
89+
if sig_len == curve.verifying_key_length:
90+
point = klass._from_raw_encoding(string, curve, validate_point)
91+
elif sig_len == curve.verifying_key_length + 1:
92+
if string[:1] != b('\x04'):
93+
raise MalformedPoint("Invalid uncompressed encoding of the "
94+
"public point")
95+
point = klass._from_raw_encoding(string[1:], curve, validate_point)
96+
elif sig_len == curve.baselen + 1:
97+
point = klass._from_compressed(string, curve, validate_point)
98+
else:
99+
raise MalformedPoint("Length of string does not match lengths of "
100+
"any of the supported encodings of {0} "
101+
"curve.".format(curve.name))
102+
57103
return klass.from_public_point(point, curve, hashfunc)
58104

59105
@classmethod
@@ -110,15 +156,32 @@ def from_public_key_recovery_with_digest(klass, signature, digest, curve, hashfu
110156
verifying_keys = [klass.from_public_point(pk.point, curve, hashfunc) for pk in pks]
111157
return verifying_keys
112158

113-
def to_string(self):
114-
# VerifyingKey.from_string(vk.to_string()) == vk as long as the
115-
# curves are the same: the curve itself is not included in the
116-
# serialized form
159+
def _raw_encode(self):
117160
order = self.pubkey.order
118161
x_str = number_to_string(self.pubkey.point.x(), order)
119162
y_str = number_to_string(self.pubkey.point.y(), order)
120163
return x_str + y_str
121164

165+
def _compressed_encode(self):
166+
order = self.pubkey.order
167+
x_str = number_to_string(self.pubkey.point.x(), order)
168+
if self.pubkey.point.y() & 1:
169+
return b('\x03') + x_str
170+
else:
171+
return b('\x02') + x_str
172+
173+
def to_string(self, encoding="raw"):
174+
# VerifyingKey.from_string(vk.to_string()) == vk as long as the
175+
# curves are the same: the curve itself is not included in the
176+
# serialized form
177+
assert encoding in ("raw", "uncompressed", "compressed")
178+
if encoding == "raw":
179+
return self._raw_encode()
180+
elif encoding == "uncompressed":
181+
return b('\x04') + self._raw_encode()
182+
else:
183+
return self._compressed_encode()
184+
122185
def to_pem(self):
123186
return der.topem(self.to_der(), "PUBLIC KEY")
124187

src/ecdsa/numbertheory.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111

1212
from __future__ import division
1313

14-
from six import integer_types
14+
from six import integer_types, PY3
1515
from six.moves import reduce
16+
try:
17+
xrange
18+
except NameError:
19+
xrange = range
1620

1721
import math
1822

@@ -62,7 +66,7 @@ def polynomial_reduce_mod(poly, polymod, p):
6266

6367
while len(poly) >= len(polymod):
6468
if poly[-1] != 0:
65-
for i in range(2, len(polymod) + 1):
69+
for i in xrange(2, len(polymod) + 1):
6670
poly[-i] = (poly[-i] - poly[-1] * polymod[-i]) % p
6771
poly = poly[0:-1]
6872

@@ -86,8 +90,8 @@ def polynomial_multiply_mod(m1, m2, polymod, p):
8690

8791
# Add together all the cross-terms:
8892

89-
for i in range(len(m1)):
90-
for j in range(len(m2)):
93+
for i in xrange(len(m1)):
94+
for j in xrange(len(m2)):
9195
prod[i + j] = (prod[i + j] + m1[i] * m2[j]) % p
9296

9397
return polynomial_reduce_mod(prod, polymod, p)
@@ -187,7 +191,12 @@ def square_root_mod_prime(a, p):
187191
return (2 * a * modular_exp(4 * a, (p - 5) // 8, p)) % p
188192
raise RuntimeError("Shouldn't get here.")
189193

190-
for b in range(2, p):
194+
if PY3:
195+
range_top = p
196+
else:
197+
# xrange on python2 can take integers representable as C long only
198+
range_top = min(0x7fffffff, p)
199+
for b in xrange(2, range_top):
191200
if jacobi(b * b - 4 * a, p) == -1:
192201
f = (a, -b, 1)
193202
ff = polynomial_exp_mod((0, 1), (p + 1) // 2, f, p)
@@ -355,7 +364,7 @@ def carmichael_of_factorized(f_list):
355364
return 1
356365

357366
result = carmichael_of_ppower(f_list[0])
358-
for i in range(1, len(f_list)):
367+
for i in xrange(1, len(f_list)):
359368
result = lcm(result, carmichael_of_ppower(f_list[i]))
360369

361370
return result
@@ -477,7 +486,7 @@ def is_prime(n):
477486
while (r % 2) == 0:
478487
s = s + 1
479488
r = r // 2
480-
for i in range(t):
489+
for i in xrange(t):
481490
a = smallprimes[i]
482491
y = modular_exp(a, r, n)
483492
if y != 1 and y != n - 1:

src/ecdsa/test_pyecdsa.py

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import with_statement, division
22

3-
import unittest
3+
try:
4+
import unittest2 as unittest
5+
except ImportError:
6+
import unittest
47
import os
58
import time
69
import shutil
@@ -11,12 +14,14 @@
1114

1215
from six import b, print_, binary_type
1316
from .keys import SigningKey, VerifyingKey
14-
from .keys import BadSignatureError
17+
from .keys import BadSignatureError, MalformedPoint
1518
from . import util
1619
from .util import sigencode_der, sigencode_strings
1720
from .util import sigdecode_der, sigdecode_strings
21+
from .util import number_to_string
1822
from .curves import Curve, UnknownCurveError
19-
from .curves import NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, SECP256k1
23+
from .curves import NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, \
24+
SECP256k1, curves
2025
from .ellipticcurve import Point
2126
from . import der
2227
from . import rfc6979
@@ -367,6 +372,99 @@ def test_public_key_recovery_with_custom_hash(self):
367372
self.assertTrue(vk.pubkey.point in
368373
[recovered_vk.pubkey.point for recovered_vk in recovered_vks])
369374

375+
def test_encoding(self):
376+
sk = SigningKey.from_secret_exponent(123456789)
377+
vk = sk.verifying_key
378+
379+
exp = b('\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3'
380+
'\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4'
381+
'z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*')
382+
self.assertEqual(vk.to_string(), exp)
383+
self.assertEqual(vk.to_string('uncompressed'), b('\x04') + exp)
384+
self.assertEqual(vk.to_string('compressed'), b('\x02') + exp[:24])
385+
386+
def test_decoding(self):
387+
sk = SigningKey.from_secret_exponent(123456789)
388+
vk = sk.verifying_key
389+
390+
enc = b('\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3'
391+
'\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4'
392+
'z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*')
393+
394+
from_raw = VerifyingKey.from_string(enc)
395+
self.assertEqual(from_raw.pubkey.point, vk.pubkey.point)
396+
397+
from_uncompressed = VerifyingKey.from_string(b('\x04') + enc)
398+
self.assertEqual(from_uncompressed.pubkey.point, vk.pubkey.point)
399+
400+
from_compressed = VerifyingKey.from_string(b('\x02') + enc[:24])
401+
self.assertEqual(from_compressed.pubkey.point, vk.pubkey.point)
402+
403+
def test_decoding_with_malformed_uncompressed(self):
404+
enc = b('\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3'
405+
'\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4'
406+
'z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*')
407+
408+
with self.assertRaises(MalformedPoint):
409+
VerifyingKey.from_string(b('\x02') + enc)
410+
411+
def test_decoding_with_malformed_compressed(self):
412+
enc = b('\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3'
413+
'\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4'
414+
'z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*')
415+
416+
with self.assertRaises(MalformedPoint):
417+
VerifyingKey.from_string(b('\x01') + enc[:24])
418+
419+
def test_decoding_with_point_at_infinity(self):
420+
# decoding it is unsupported, as it's not necessary to encode it
421+
with self.assertRaises(MalformedPoint):
422+
VerifyingKey.from_string(b('\x00'))
423+
424+
def test_not_lying_on_curve(self):
425+
enc = number_to_string(NIST192p.order, NIST192p.order+1)
426+
427+
with self.assertRaises(MalformedPoint):
428+
VerifyingKey.from_string(b('\x02') + enc)
429+
430+
431+
@pytest.mark.parametrize("val,even",
432+
[(i, j) for i in range(256) for j in [True, False]])
433+
def test_VerifyingKey_decode_with_small_values(val, even):
434+
enc = number_to_string(val, NIST192p.order)
435+
436+
if even:
437+
enc = b('\x02') + enc
438+
else:
439+
enc = b('\x03') + enc
440+
441+
# small values can both be actual valid public keys and not, verify that
442+
# only expected exceptions are raised if they are not
443+
try:
444+
vk = VerifyingKey.from_string(enc)
445+
assert isinstance(vk, VerifyingKey)
446+
except MalformedPoint:
447+
assert True
448+
449+
450+
params = []
451+
for curve in curves:
452+
for enc in ["raw", "uncompressed", "compressed"]:
453+
params.append(pytest.param(curve, enc, id="{0}-{1}".format(
454+
curve.name, enc)))
455+
456+
457+
@pytest.mark.parametrize("curve,encoding", params)
458+
def test_VerifyingKey_encode_decode(curve, encoding):
459+
sk = SigningKey.generate(curve=curve)
460+
vk = sk.verifying_key
461+
462+
encoded = vk.to_string(encoding)
463+
464+
from_enc = VerifyingKey.from_string(encoded, curve=curve)
465+
466+
assert vk.pubkey.point == from_enc.pubkey.point
467+
370468

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

0 commit comments

Comments
 (0)