Skip to content

Commit 078882e

Browse files
committed
add support for reading and writing curve parameters in DER
1 parent 315f312 commit 078882e

File tree

5 files changed

+436
-56
lines changed

5 files changed

+436
-56
lines changed

src/ecdsa/curves.py

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from __future__ import division
22

3-
from . import der, ecdsa
4-
from .util import orderlen
3+
from . import der, ecdsa, ellipticcurve
4+
from .util import orderlen, number_to_string, string_to_number
5+
from ._compat import normalise_bytes
56

67

78
# orderlen was defined in this module previously, so keep it in __all__,
@@ -29,9 +30,15 @@
2930
"BRAINPOOLP320r1",
3031
"BRAINPOOLP384r1",
3132
"BRAINPOOLP512r1",
33+
"PRIME_FIELD_OID",
34+
"CHARACTERISTIC_TWO_FIELD_OID",
3235
]
3336

3437

38+
PRIME_FIELD_OID = (1, 2, 840, 10045, 1, 1)
39+
CHARACTERISTIC_TWO_FIELD_OID = (1, 2, 840, 10045, 1, 2)
40+
41+
3542
class UnknownCurveError(Exception):
3643
pass
3744

@@ -47,11 +54,142 @@ def __init__(self, name, curve, generator, oid, openssl_name=None):
4754
self.verifying_key_length = 2 * orderlen(curve.p())
4855
self.signature_length = 2 * self.baselen
4956
self.oid = oid
50-
self.encoded_oid = der.encode_oid(*oid)
57+
if oid:
58+
self.encoded_oid = der.encode_oid(*oid)
59+
60+
def __eq__(self, other):
61+
if isinstance(other, Curve):
62+
return (
63+
self.curve == other.curve and self.generator == other.generator
64+
)
65+
return NotImplemented
66+
67+
def __ne__(self, other):
68+
return not self == other
5169

5270
def __repr__(self):
5371
return self.name
5472

73+
def to_der(self, encoding=None, point_encoding="uncompressed"):
74+
"""Serialise the curve parameters to binary string.
75+
76+
:param str encoding: the format to save the curve parameters in.
77+
Default is ``named_curve``, with fallback being the ``explicit``
78+
if the OID is not set for the curve.
79+
:param str point_encoding: the point encoding of the generator when
80+
explicit curve encoding is used. Ignored for ``named_curve``
81+
format.
82+
"""
83+
if encoding is None:
84+
if self.oid:
85+
encoding = "named_curve"
86+
else:
87+
encoding = "explicit"
88+
89+
if encoding == "named_curve":
90+
if not self.oid:
91+
raise UnknownCurveError(
92+
"Can't encode curve using named_curve encoding without "
93+
"associated curve OID"
94+
)
95+
return der.encode_oid(*self.oid)
96+
97+
# encode the ECParameters sequence
98+
curve_p = self.curve.p()
99+
version = der.encode_integer(1)
100+
field_id = der.encode_sequence(
101+
der.encode_oid(*PRIME_FIELD_OID), der.encode_integer(curve_p)
102+
)
103+
curve = der.encode_sequence(
104+
der.encode_octet_string(
105+
number_to_string(self.curve.a() % curve_p, curve_p)
106+
),
107+
der.encode_octet_string(
108+
number_to_string(self.curve.b() % curve_p, curve_p)
109+
),
110+
)
111+
base = der.encode_octet_string(self.generator.to_bytes(point_encoding))
112+
order = der.encode_integer(self.generator.order())
113+
seq_elements = [version, field_id, curve, base, order]
114+
if self.curve.cofactor():
115+
cofactor = der.encode_integer(self.curve.cofactor())
116+
seq_elements.append(cofactor)
117+
118+
return der.encode_sequence(*seq_elements)
119+
120+
@staticmethod
121+
def from_der(data):
122+
"""Decode the curve parameters from DER file.
123+
124+
:param data: the binary string to decode the parameters from
125+
:type data: bytes-like object
126+
"""
127+
data = normalise_bytes(data)
128+
if not der.is_sequence(data):
129+
oid, empty = der.remove_object(data)
130+
if empty:
131+
raise der.UnexpectedDER("Unexpected data after OID")
132+
return find_curve(oid)
133+
134+
seq, empty = der.remove_sequence(data)
135+
if empty:
136+
raise der.UnexpectedDER(
137+
"Unexpected data after ECParameters structure"
138+
)
139+
# decode the ECParameters sequence
140+
version, rest = der.remove_integer(seq)
141+
if version != 1:
142+
raise der.UnexpectedDER("Unknown parameter encoding format")
143+
field_id, rest = der.remove_sequence(rest)
144+
curve, rest = der.remove_sequence(rest)
145+
base_bytes, rest = der.remove_octet_string(rest)
146+
order, rest = der.remove_integer(rest)
147+
cofactor = None
148+
if rest:
149+
cofactor, rest = der.remove_integer(rest)
150+
151+
# decode the ECParameters.fieldID sequence
152+
field_type, rest = der.remove_object(field_id)
153+
if field_type == CHARACTERISTIC_TWO_FIELD_OID:
154+
raise UnknownCurveError("Characteristic 2 curves unsupported")
155+
if field_type != PRIME_FIELD_OID:
156+
raise UnknownCurveError(
157+
"Unknown field type: {0}".format(field_type)
158+
)
159+
prime, empty = der.remove_integer(rest)
160+
if empty:
161+
raise der.UnexpectedDER(
162+
"Unexpected data after ECParameters.fieldID.Prime-p element"
163+
)
164+
165+
# decode the ECParameters.curve sequence
166+
curve_a_bytes, rest = der.remove_octet_string(curve)
167+
curve_b_bytes, rest = der.remove_octet_string(rest)
168+
# seed can be defined here, but we don't parse it, so ignore `rest`
169+
170+
curve_a = string_to_number(curve_a_bytes)
171+
curve_b = string_to_number(curve_b_bytes)
172+
173+
curve_fp = ellipticcurve.CurveFp(prime, curve_a, curve_b, cofactor)
174+
175+
# decode the ECParameters.base point
176+
177+
base = ellipticcurve.PointJacobi.from_bytes(
178+
curve_fp,
179+
base_bytes,
180+
valid_encodings=("uncompressed", "compressed", "hybrid"),
181+
order=order,
182+
generator=True,
183+
)
184+
tmp_curve = Curve("unknown", curve_fp, base, None)
185+
186+
# if the curve matches one of the well-known ones, use the well-known
187+
# one in preference, as it will have the OID and name associated
188+
for i in curves:
189+
if tmp_curve == i:
190+
return i
191+
return tmp_curve
192+
55193

56194
# the SEC curves
57195
SECP112r1 = Curve(

src/ecdsa/ellipticcurve.py

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
from . import numbertheory
5252
from ._compat import normalise_bytes
5353
from .errors import MalformedPointError
54-
from .util import orderlen, string_to_number
54+
from .util import orderlen, string_to_number, number_to_string
5555

5656

5757
@python_2_unicode_compatible
@@ -101,10 +101,11 @@ def __eq__(self, other):
101101
only the prime and curve parameters are considered.
102102
"""
103103
if isinstance(other, CurveFp):
104+
p = self.__p
104105
return (
105106
self.__p == other.__p
106-
and self.__a == other.__a
107-
and self.__b == other.__b
107+
and self.__a % p == other.__a % p
108+
and self.__b % p == other.__b % p
108109
)
109110
return NotImplemented
110111

@@ -142,6 +143,7 @@ def __str__(self):
142143

143144
class AbstractPoint(object):
144145
"""Class for common methods of elliptic curve points."""
146+
145147
@staticmethod
146148
def _from_raw_encoding(data, raw_encoding_length):
147149
"""
@@ -207,11 +209,7 @@ def _from_hybrid(cls, data, raw_encoding_length, validate_encoding):
207209

208210
@classmethod
209211
def from_bytes(
210-
cls,
211-
curve,
212-
data,
213-
validate_encoding=True,
214-
valid_encodings=None
212+
cls, curve, data, validate_encoding=True, valid_encodings=None
215213
):
216214
"""
217215
Initialise the object from byte encoding of a point.
@@ -265,17 +263,14 @@ def from_bytes(
265263
elif key_len == raw_encoding_length + 1 and (
266264
"hybrid" in valid_encodings or "uncompressed" in valid_encodings
267265
):
268-
if (
269-
data[:1] in (b"\x06", b"\x07")
270-
and "hybrid" in valid_encodings
271-
):
266+
if data[:1] in (b"\x06", b"\x07") and "hybrid" in valid_encodings:
272267
coord_x, coord_y = cls._from_hybrid(
273268
data, raw_encoding_length, validate_encoding
274269
)
275270
elif data[:1] == b"\x04" and "uncompressed" in valid_encodings:
276-
coord_x, coord_y = cls._from_raw_encoding(
271+
coord_x, coord_y = cls._from_raw_encoding(
277272
data[1:], raw_encoding_length
278-
)
273+
)
279274
else:
280275
raise MalformedPointError(
281276
"Invalid X9.62 encoding of the public point"
@@ -293,6 +288,49 @@ def from_bytes(
293288
)
294289
return coord_x, coord_y
295290

291+
def _raw_encode(self):
292+
"""Convert the point to the :term:`raw encoding`."""
293+
prime = self.curve().p()
294+
x_str = number_to_string(self.x(), prime)
295+
y_str = number_to_string(self.y(), prime)
296+
return x_str + y_str
297+
298+
def _compressed_encode(self):
299+
"""Encode the point into the compressed form."""
300+
prime = self.curve().p()
301+
x_str = number_to_string(self.x(), prime)
302+
if self.y() & 1:
303+
return b"\x03" + x_str
304+
return b"\x02" + x_str
305+
306+
def _hybrid_encode(self):
307+
"""Encode the point into the hybrid form."""
308+
raw_enc = self._raw_encode()
309+
if self.y() & 1:
310+
return b"\x07" + raw_enc
311+
return b"\x06" + raw_enc
312+
313+
def to_bytes(self, encoding="raw"):
314+
"""
315+
Convert the point to a byte string.
316+
317+
The method by default uses the :term:`raw encoding` (specified
318+
by `encoding="raw"`. It can also output points in :term:`uncompressed`,
319+
:term:`compressed`, and :term:`hybrid` formats.
320+
321+
:return: :term:`raw encoding` of a public on the curve
322+
:rtype: bytes
323+
"""
324+
assert encoding in ("raw", "uncompressed", "compressed", "hybrid")
325+
if encoding == "raw":
326+
return self._raw_encode()
327+
elif encoding == "uncompressed":
328+
return b"\x04" + self._raw_encode()
329+
elif encoding == "hybrid":
330+
return self._hybrid_encode()
331+
else:
332+
return self._compressed_encode()
333+
296334

297335
class PointJacobi(AbstractPoint):
298336
"""
@@ -341,7 +379,7 @@ def from_bytes(
341379
validate_encoding=True,
342380
valid_encodings=None,
343381
order=None,
344-
generator=False
382+
generator=False,
345383
):
346384
"""
347385
Initialise the object from byte encoding of a point.
@@ -920,7 +958,7 @@ def from_bytes(
920958
data,
921959
validate_encoding=True,
922960
valid_encodings=None,
923-
order=None
961+
order=None,
924962
):
925963
"""
926964
Initialise the object from byte encoding of a point.
@@ -956,7 +994,6 @@ def from_bytes(
956994
)
957995
return Point(curve, coord_x, coord_y, order)
958996

959-
960997
def __eq__(self, other):
961998
"""Return True if the points are identical, False otherwise.
962999

src/ecdsa/errors.py

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

44
pass
5-
6-

src/ecdsa/keys.py

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@
7878
from . import rfc6979
7979
from . import ellipticcurve
8080
from .curves import NIST192p, find_curve
81-
from .numbertheory import square_root_mod_prime, SquareRootError
8281
from .ecdsa import RSZeroError
8382
from .util import string_to_number, number_to_string, randrange
8483
from .util import sigencode_string, sigdecode_string, bit_length
@@ -311,7 +310,7 @@ def from_string(
311310
curve.curve,
312311
string,
313312
validate_encoding=validate_point,
314-
valid_encodings=valid_encodings
313+
valid_encodings=valid_encodings,
315314
)
316315
return cls.from_public_point(point, curve, hashfunc, validate_point)
317316

@@ -526,30 +525,6 @@ def from_public_key_recovery_with_digest(
526525
]
527526
return verifying_keys
528527

529-
def _raw_encode(self):
530-
"""Convert the public key to the :term:`raw encoding`."""
531-
order = self.curve.curve.p()
532-
x_str = number_to_string(self.pubkey.point.x(), order)
533-
y_str = number_to_string(self.pubkey.point.y(), order)
534-
return x_str + y_str
535-
536-
def _compressed_encode(self):
537-
"""Encode the public point into the compressed form."""
538-
order = self.curve.curve.p()
539-
x_str = number_to_string(self.pubkey.point.x(), order)
540-
if self.pubkey.point.y() & 1:
541-
return b("\x03") + x_str
542-
else:
543-
return b("\x02") + x_str
544-
545-
def _hybrid_encode(self):
546-
"""Encode the public point into the hybrid form."""
547-
raw_enc = self._raw_encode()
548-
if self.pubkey.point.y() & 1:
549-
return b("\x07") + raw_enc
550-
else:
551-
return b("\x06") + raw_enc
552-
553528
def to_string(self, encoding="raw"):
554529
"""
555530
Convert the public key to a byte string.
@@ -571,14 +546,7 @@ def to_string(self, encoding="raw"):
571546
:rtype: bytes
572547
"""
573548
assert encoding in ("raw", "uncompressed", "compressed", "hybrid")
574-
if encoding == "raw":
575-
return self._raw_encode()
576-
elif encoding == "uncompressed":
577-
return b("\x04") + self._raw_encode()
578-
elif encoding == "hybrid":
579-
return self._hybrid_encode()
580-
else:
581-
return self._compressed_encode()
549+
return self.pubkey.point.to_bytes(encoding)
582550

583551
def to_pem(self, point_encoding="uncompressed"):
584552
"""

0 commit comments

Comments
 (0)