Skip to content

Commit 315f312

Browse files
committed
move parsing of points from VerifyingKey to parent class of points
For decoding points it's not necessary to have all the data useful for decoding public keys. This will also make it possible to decode explicit EC parameters, as decoding of a public key requires knowledge of the curve's base point and the base point is in defined in the parameters, creating a chicken and an egg problem with using the VerifyingKey.from_string() to parse the base point.
1 parent a385c44 commit 315f312

File tree

4 files changed

+273
-107
lines changed

4 files changed

+273
-107
lines changed

src/ecdsa/ellipticcurve.py

Lines changed: 252 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949

5050
from six import python_2_unicode_compatible
5151
from . import numbertheory
52+
from ._compat import normalise_bytes
53+
from .errors import MalformedPointError
54+
from .util import orderlen, string_to_number
5255

5356

5457
@python_2_unicode_compatible
@@ -137,7 +140,161 @@ def __str__(self):
137140
)
138141

139142

140-
class PointJacobi(object):
143+
class AbstractPoint(object):
144+
"""Class for common methods of elliptic curve points."""
145+
@staticmethod
146+
def _from_raw_encoding(data, raw_encoding_length):
147+
"""
148+
Decode public point from :term:`raw encoding`.
149+
150+
:term:`raw encoding` is the same as the :term:`uncompressed` encoding,
151+
but without the 0x04 byte at the beginning.
152+
"""
153+
# real assert, from_bytes() should not call us with different length
154+
assert len(data) == raw_encoding_length
155+
xs = data[: raw_encoding_length // 2]
156+
ys = data[raw_encoding_length // 2 :]
157+
# real assert, raw_encoding_length is calculated by multiplying an
158+
# integer by two so it will always be even
159+
assert len(xs) == raw_encoding_length // 2
160+
assert len(ys) == raw_encoding_length // 2
161+
coord_x = string_to_number(xs)
162+
coord_y = string_to_number(ys)
163+
164+
return coord_x, coord_y
165+
166+
@staticmethod
167+
def _from_compressed(data, curve):
168+
"""Decode public point from compressed encoding."""
169+
if data[:1] not in (b"\x02", b"\x03"):
170+
raise MalformedPointError("Malformed compressed point encoding")
171+
172+
is_even = data[:1] == b"\x02"
173+
x = string_to_number(data[1:])
174+
p = curve.p()
175+
alpha = (pow(x, 3, p) + (curve.a() * x) + curve.b()) % p
176+
try:
177+
beta = numbertheory.square_root_mod_prime(alpha, p)
178+
except numbertheory.SquareRootError as e:
179+
raise MalformedPointError(
180+
"Encoding does not correspond to a point on curve", e
181+
)
182+
if is_even == bool(beta & 1):
183+
y = p - beta
184+
else:
185+
y = beta
186+
return x, y
187+
188+
@classmethod
189+
def _from_hybrid(cls, data, raw_encoding_length, validate_encoding):
190+
"""Decode public point from hybrid encoding."""
191+
# real assert, from_bytes() should not call us with different types
192+
assert data[:1] in (b"\x06", b"\x07")
193+
194+
# primarily use the uncompressed as it's easiest to handle
195+
x, y = cls._from_raw_encoding(data[1:], raw_encoding_length)
196+
197+
# but validate if it's self-consistent if we're asked to do that
198+
if validate_encoding and (
199+
y & 1
200+
and data[:1] != b"\x07"
201+
or (not y & 1)
202+
and data[:1] != b"\x06"
203+
):
204+
raise MalformedPointError("Inconsistent hybrid point encoding")
205+
206+
return x, y
207+
208+
@classmethod
209+
def from_bytes(
210+
cls,
211+
curve,
212+
data,
213+
validate_encoding=True,
214+
valid_encodings=None
215+
):
216+
"""
217+
Initialise the object from byte encoding of a point.
218+
219+
The method does accept and automatically detect the type of point
220+
encoding used. It supports the :term:`raw encoding`,
221+
:term:`uncompressed`, :term:`compressed`, and :term:`hybrid` encodings.
222+
223+
Note: generally you will want to call the ``from_bytes()`` method of
224+
either a child class, PointJacobi or Point.
225+
226+
:param data: single point encoding of the public key
227+
:type data: :term:`bytes-like object`
228+
:param curve: the curve on which the public key is expected to lay
229+
:type curve: ecdsa.ellipticcurve.CurveFp
230+
:param validate_encoding: whether to verify that the encoding of the
231+
point is self-consistent, defaults to True, has effect only
232+
on ``hybrid`` encoding
233+
:type validate_encoding: bool
234+
:param valid_encodings: list of acceptable point encoding formats,
235+
supported ones are: :term:`uncompressed`, :term:`compressed`,
236+
:term:`hybrid`, and :term:`raw encoding` (specified with ``raw``
237+
name). All formats by default (specified with ``None``).
238+
:type valid_encodings: :term:`set-like object`
239+
240+
:raises MalformedPointError: if the public point does not lay on the
241+
curve or the encoding is invalid
242+
243+
:return: x and y coordinates of the encoded point
244+
:rtype: tuple(int, int)
245+
"""
246+
if not valid_encodings:
247+
valid_encodings = set(
248+
["uncompressed", "compressed", "hybrid", "raw"]
249+
)
250+
if not all(
251+
i in set(("uncompressed", "compressed", "hybrid", "raw"))
252+
for i in valid_encodings
253+
):
254+
raise ValueError(
255+
"Only uncompressed, compressed, hybrid or raw encoding "
256+
"supported."
257+
)
258+
data = normalise_bytes(data)
259+
key_len = len(data)
260+
raw_encoding_length = 2 * orderlen(curve.p())
261+
if key_len == raw_encoding_length and "raw" in valid_encodings:
262+
coord_x, coord_y = cls._from_raw_encoding(
263+
data, raw_encoding_length
264+
)
265+
elif key_len == raw_encoding_length + 1 and (
266+
"hybrid" in valid_encodings or "uncompressed" in valid_encodings
267+
):
268+
if (
269+
data[:1] in (b"\x06", b"\x07")
270+
and "hybrid" in valid_encodings
271+
):
272+
coord_x, coord_y = cls._from_hybrid(
273+
data, raw_encoding_length, validate_encoding
274+
)
275+
elif data[:1] == b"\x04" and "uncompressed" in valid_encodings:
276+
coord_x, coord_y = cls._from_raw_encoding(
277+
data[1:], raw_encoding_length
278+
)
279+
else:
280+
raise MalformedPointError(
281+
"Invalid X9.62 encoding of the public point"
282+
)
283+
elif (
284+
key_len == raw_encoding_length // 2 + 1
285+
and "compressed" in valid_encodings
286+
):
287+
coord_x, coord_y = cls._from_compressed(data, curve)
288+
else:
289+
raise MalformedPointError(
290+
"Length of string does not match lengths of "
291+
"any of the enabled ({0}) encodings of the "
292+
"curve.".format(", ".join(valid_encodings))
293+
)
294+
return coord_x, coord_y
295+
296+
297+
class PointJacobi(AbstractPoint):
141298
"""
142299
Point on an elliptic curve. Uses Jacobi coordinates.
143300
@@ -165,6 +322,7 @@ def __init__(self, curve, x, y, z, order=None, generator=False):
165322
such, it will be commonly used with scalar multiplication. This will
166323
cause to precompute multiplication table generation for it
167324
"""
325+
super(PointJacobi, self).__init__()
168326
self.__curve = curve
169327
if GMPY: # pragma: no branch
170328
self.__coords = (mpz(x), mpz(y), mpz(z))
@@ -175,6 +333,53 @@ def __init__(self, curve, x, y, z, order=None, generator=False):
175333
self.__generator = generator
176334
self.__precompute = []
177335

336+
@classmethod
337+
def from_bytes(
338+
cls,
339+
curve,
340+
data,
341+
validate_encoding=True,
342+
valid_encodings=None,
343+
order=None,
344+
generator=False
345+
):
346+
"""
347+
Initialise the object from byte encoding of a point.
348+
349+
The method does accept and automatically detect the type of point
350+
encoding used. It supports the :term:`raw encoding`,
351+
:term:`uncompressed`, :term:`compressed`, and :term:`hybrid` encodings.
352+
353+
:param data: single point encoding of the public key
354+
:type data: :term:`bytes-like object`
355+
:param curve: the curve on which the public key is expected to lay
356+
:type curve: ecdsa.ellipticcurve.CurveFp
357+
:param validate_encoding: whether to verify that the encoding of the
358+
point is self-consistent, defaults to True, has effect only
359+
on ``hybrid`` encoding
360+
:type validate_encoding: bool
361+
:param valid_encodings: list of acceptable point encoding formats,
362+
supported ones are: :term:`uncompressed`, :term:`compressed`,
363+
:term:`hybrid`, and :term:`raw encoding` (specified with ``raw``
364+
name). All formats by default (specified with ``None``).
365+
:type valid_encodings: :term:`set-like object`
366+
:param int order: the point order, must be non zero when using
367+
generator=True
368+
:param bool generator: the point provided is a curve generator, as
369+
such, it will be commonly used with scalar multiplication. This
370+
will cause to precompute multiplication table generation for it
371+
372+
:raises MalformedPointError: if the public point does not lay on the
373+
curve or the encoding is invalid
374+
375+
:return: Point on curve
376+
:rtype: PointJacobi
377+
"""
378+
coord_x, coord_y = super(PointJacobi, cls).from_bytes(
379+
curve, data, validate_encoding, valid_encodings
380+
)
381+
return PointJacobi(curve, coord_x, coord_y, 1, order, generator)
382+
178383
def _maybe_precompute(self):
179384
if not self.__generator or self.__precompute:
180385
return
@@ -683,12 +888,13 @@ def __neg__(self):
683888
return PointJacobi(self.__curve, x, -y, z, self.__order)
684889

685890

686-
class Point(object):
891+
class Point(AbstractPoint):
687892
"""A point on an elliptic curve. Altering x and y is forbidden,
688893
but they can be read by the x() and y() methods."""
689894

690895
def __init__(self, curve, x, y, order=None):
691896
"""curve, x, y, order; order (optional) is the order of this point."""
897+
super(Point, self).__init__()
692898
self.__curve = curve
693899
if GMPY:
694900
self.__x = x and mpz(x)
@@ -707,6 +913,50 @@ def __init__(self, curve, x, y, order=None):
707913
if curve and curve.cofactor() != 1 and order:
708914
assert self * order == INFINITY
709915

916+
@classmethod
917+
def from_bytes(
918+
cls,
919+
curve,
920+
data,
921+
validate_encoding=True,
922+
valid_encodings=None,
923+
order=None
924+
):
925+
"""
926+
Initialise the object from byte encoding of a point.
927+
928+
The method does accept and automatically detect the type of point
929+
encoding used. It supports the :term:`raw encoding`,
930+
:term:`uncompressed`, :term:`compressed`, and :term:`hybrid` encodings.
931+
932+
:param data: single point encoding of the public key
933+
:type data: :term:`bytes-like object`
934+
:param curve: the curve on which the public key is expected to lay
935+
:type curve: ecdsa.ellipticcurve.CurveFp
936+
:param validate_encoding: whether to verify that the encoding of the
937+
point is self-consistent, defaults to True, has effect only
938+
on ``hybrid`` encoding
939+
:type validate_encoding: bool
940+
:param valid_encodings: list of acceptable point encoding formats,
941+
supported ones are: :term:`uncompressed`, :term:`compressed`,
942+
:term:`hybrid`, and :term:`raw encoding` (specified with ``raw``
943+
name). All formats by default (specified with ``None``).
944+
:type valid_encodings: :term:`set-like object`
945+
:param int order: the point order, must be non zero when using
946+
generator=True
947+
948+
:raises MalformedPointError: if the public point does not lay on the
949+
curve or the encoding is invalid
950+
951+
:return: Point on curve
952+
:rtype: Point
953+
"""
954+
coord_x, coord_y = super(Point, cls).from_bytes(
955+
curve, data, validate_encoding, valid_encodings
956+
)
957+
return Point(curve, coord_x, coord_y, order)
958+
959+
710960
def __eq__(self, other):
711961
"""Return True if the points are identical, False otherwise.
712962

src/ecdsa/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class MalformedPointError(AssertionError):
2+
"""Raised in case the encoding of private or public key is malformed."""
3+
4+
pass
5+
6+

0 commit comments

Comments
 (0)