Skip to content

Commit b4c4203

Browse files
authored
Merge pull request #251 from tomato42/raw-encoding
allow limiting point formats, don't accept malformed PEM public files
2 parents cd66a2a + a13b1d0 commit b4c4203

File tree

4 files changed

+137
-16
lines changed

4 files changed

+137
-16
lines changed

src/ecdsa/ecdh.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def get_public_key(self):
216216
217217
:return: public (verifying) key from local private key.
218218
:rtype: VerifyingKey object
219-
"""
219+
"""
220220
return self.private_key.get_verifying_key()
221221

222222
def load_received_public_key(self, public_key):
@@ -237,7 +237,9 @@ def load_received_public_key(self, public_key):
237237
raise InvalidCurveError("Curve mismatch.")
238238
self.public_key = public_key
239239

240-
def load_received_public_key_bytes(self, public_key_str):
240+
def load_received_public_key_bytes(
241+
self, public_key_str, valid_encodings=None
242+
):
241243
"""
242244
Load public key from byte string.
243245
@@ -247,9 +249,16 @@ def load_received_public_key_bytes(self, public_key_str):
247249
248250
:param public_key_str: public key in bytes string format
249251
:type public_key_str: :term:`bytes-like object`
252+
:param valid_encodings: list of acceptable point encoding formats,
253+
supported ones are: :term:`uncompressed`, :term:`compressed`,
254+
:term:`hybrid`, and :term:`raw encoding` (specified with ``raw``
255+
name). All formats by default (specified with ``None``).
256+
:type valid_encodings: :term:`set-like object`
250257
"""
251258
return self.load_received_public_key(
252-
VerifyingKey.from_string(public_key_str, self.curve)
259+
VerifyingKey.from_string(
260+
public_key_str, self.curve, valid_encodings
261+
)
253262
)
254263

255264
def load_received_public_key_der(self, public_key_der):

src/ecdsa/keys.py

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@
6464
string) is endianess dependant! Signature computed over ``array.array``
6565
of integers on a big-endian system will not be verified on a
6666
little-endian system and vice-versa.
67+
68+
set-like object
69+
All the types that support the ``in`` operator, like ``list``,
70+
``tuple``, ``set``, ``frozenset``, etc.
6771
"""
6872

6973
import binascii
@@ -332,7 +336,12 @@ def _from_hybrid(cls, string, curve, validate_point):
332336

333337
@classmethod
334338
def from_string(
335-
cls, string, curve=NIST192p, hashfunc=sha1, validate_point=True
339+
cls,
340+
string,
341+
curve=NIST192p,
342+
hashfunc=sha1,
343+
validate_point=True,
344+
valid_encodings=None,
336345
):
337346
"""
338347
Initialise the object from byte encoding of public key.
@@ -355,38 +364,55 @@ def from_string(
355364
:param validate_point: whether to verify that the point lays on the
356365
provided curve or not, defaults to True
357366
:type validate_point: bool
367+
:param valid_encodings: list of acceptable point encoding formats,
368+
supported ones are: :term:`uncompressed`, :term:`compressed`,
369+
:term:`hybrid`, and :term:`raw encoding` (specified with ``raw``
370+
name). All formats by default (specified with ``None``).
371+
:type valid_encodings: :term:`set-like object`
358372
359373
:raises MalformedPointError: if the public point does not lay on the
360374
curve or the encoding is invalid
361375
362376
:return: Initialised VerifyingKey object
363377
:rtype: VerifyingKey
364378
"""
379+
if valid_encodings is None:
380+
valid_encodings = set(
381+
["uncompressed", "compressed", "hybrid", "raw"]
382+
)
365383
string = normalise_bytes(string)
366384
sig_len = len(string)
367-
if sig_len == curve.verifying_key_length:
385+
if sig_len == curve.verifying_key_length and "raw" in valid_encodings:
368386
point = cls._from_raw_encoding(string, curve)
369-
elif sig_len == curve.verifying_key_length + 1:
370-
if string[:1] in (b("\x06"), b("\x07")):
387+
elif sig_len == curve.verifying_key_length + 1 and (
388+
"hybrid" in valid_encodings or "uncompressed" in valid_encodings
389+
):
390+
if (
391+
string[:1] in (b("\x06"), b("\x07"))
392+
and "hybrid" in valid_encodings
393+
):
371394
point = cls._from_hybrid(string, curve, validate_point)
372-
elif string[:1] == b("\x04"):
395+
elif string[:1] == b("\x04") and "uncompressed" in valid_encodings:
373396
point = cls._from_raw_encoding(string[1:], curve)
374397
else:
375398
raise MalformedPointError(
376399
"Invalid X9.62 encoding of the public point"
377400
)
378-
elif sig_len == curve.verifying_key_length // 2 + 1:
401+
elif (
402+
sig_len == curve.verifying_key_length // 2 + 1
403+
and "compressed" in valid_encodings
404+
):
379405
point = cls._from_compressed(string, curve)
380406
else:
381407
raise MalformedPointError(
382408
"Length of string does not match lengths of "
383-
"any of the supported encodings of {0} "
384-
"curve.".format(curve.name)
409+
"any of the enabled ({1}) encodings of {0} "
410+
"curve.".format(curve.name, ", ".join(valid_encodings))
385411
)
386412
return cls.from_public_point(point, curve, hashfunc, validate_point)
387413

388414
@classmethod
389-
def from_pem(cls, string, hashfunc=sha1):
415+
def from_pem(cls, string, hashfunc=sha1, valid_encodings=None):
390416
"""
391417
Initialise from public key stored in :term:`PEM` format.
392418
@@ -400,14 +426,23 @@ def from_pem(cls, string, hashfunc=sha1):
400426
401427
:param string: text with PEM-encoded public ECDSA key
402428
:type string: str
429+
:param valid_encodings: list of allowed point encodings.
430+
By default :term:`uncompressed`, :term:`compressed`, and
431+
:term:`hybrid`. To read malformed files, include
432+
:term:`raw encoding` with ``raw`` in the list.
433+
:type valid_encodings: :term:`set-like object
403434
404435
:return: Initialised VerifyingKey object
405436
:rtype: VerifyingKey
406437
"""
407-
return cls.from_der(der.unpem(string), hashfunc=hashfunc)
438+
return cls.from_der(
439+
der.unpem(string),
440+
hashfunc=hashfunc,
441+
valid_encodings=valid_encodings,
442+
)
408443

409444
@classmethod
410-
def from_der(cls, string, hashfunc=sha1):
445+
def from_der(cls, string, hashfunc=sha1, valid_encodings=None):
411446
"""
412447
Initialise the key stored in :term:`DER` format.
413448
@@ -432,10 +467,17 @@ def from_der(cls, string, hashfunc=sha1):
432467
433468
:param string: binary string with the DER encoding of public ECDSA key
434469
:type string: bytes-like object
470+
:param valid_encodings: list of allowed point encodings.
471+
By default :term:`uncompressed`, :term:`compressed`, and
472+
:term:`hybrid`. To read malformed files, include
473+
:term:`raw encoding` with ``raw`` in the list.
474+
:type valid_encodings: :term:`set-like object
435475
436476
:return: Initialised VerifyingKey object
437477
:rtype: VerifyingKey
438478
"""
479+
if valid_encodings is None:
480+
valid_encodings = set(["uncompressed", "compressed", "hybrid"])
439481
string = normalise_bytes(string)
440482
# [[oid_ecPublicKey,oid_curve], point_str_bitstring]
441483
s1, empty = der.remove_sequence(string)
@@ -467,7 +509,12 @@ def from_der(cls, string, hashfunc=sha1):
467509
# raw encoding of point is invalid in DER files
468510
if len(point_str) == curve.verifying_key_length:
469511
raise der.UnexpectedDER("Malformed encoding of public point")
470-
return cls.from_string(point_str, curve, hashfunc=hashfunc)
512+
return cls.from_string(
513+
point_str,
514+
curve,
515+
hashfunc=hashfunc,
516+
valid_encodings=valid_encodings,
517+
)
471518

472519
@classmethod
473520
def from_public_key_recovery(

src/ecdsa/test_keys.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import pytest
1414
import hashlib
1515

16-
from .keys import VerifyingKey, SigningKey
16+
from .keys import VerifyingKey, SigningKey, MalformedPointError
1717
from .der import unpem
1818
from .util import (
1919
sigencode_string,
@@ -153,6 +153,12 @@ def setUpClass(cls):
153153

154154
cls.sk2 = SigningKey.generate(vk.curve)
155155

156+
def test_load_key_with_disabled_format(self):
157+
with self.assertRaises(MalformedPointError) as e:
158+
VerifyingKey.from_der(self.key_bytes, valid_encodings=["raw"])
159+
160+
self.assertIn("enabled (raw) encodings", str(e.exception))
161+
156162
def test_custom_hashfunc(self):
157163
vk = VerifyingKey.from_der(self.key_bytes, hashlib.sha256)
158164

src/ecdsa/test_pyecdsa.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,65 @@ def test_decoding(self):
722722
from_uncompressed = VerifyingKey.from_string(b("\x06") + enc)
723723
self.assertEqual(from_uncompressed.pubkey.point, vk.pubkey.point)
724724

725+
def test_uncompressed_decoding_as_only_alowed(self):
726+
enc = b(
727+
"\x04"
728+
"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3"
729+
"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4"
730+
"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*"
731+
)
732+
vk = VerifyingKey.from_string(enc, valid_encodings=("uncompressed",))
733+
sk = SigningKey.from_secret_exponent(123456789)
734+
735+
self.assertEqual(vk, sk.verifying_key)
736+
737+
def test_raw_decoding_with_blocked_format(self):
738+
enc = b(
739+
"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3"
740+
"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4"
741+
"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*"
742+
)
743+
with self.assertRaises(MalformedPointError) as exp:
744+
VerifyingKey.from_string(enc, valid_encodings=("hybrid",))
745+
746+
self.assertIn("hybrid", str(exp.exception))
747+
748+
def test_uncompressed_decoding_with_blocked_format(self):
749+
enc = b(
750+
"\x04"
751+
"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3"
752+
"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4"
753+
"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*"
754+
)
755+
with self.assertRaises(MalformedPointError) as exp:
756+
VerifyingKey.from_string(enc, valid_encodings=("hybrid",))
757+
758+
self.assertIn("Invalid X9.62 encoding", str(exp.exception))
759+
760+
def test_hybrid_decoding_with_blocked_format(self):
761+
enc = b(
762+
"\x06"
763+
"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3"
764+
"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4"
765+
"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*"
766+
)
767+
with self.assertRaises(MalformedPointError) as exp:
768+
VerifyingKey.from_string(enc, valid_encodings=("uncompressed",))
769+
770+
self.assertIn("Invalid X9.62 encoding", str(exp.exception))
771+
772+
def test_compressed_decoding_with_blocked_format(self):
773+
enc = b(
774+
"\x02"
775+
"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3"
776+
"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4"
777+
"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*"
778+
)[:25]
779+
with self.assertRaises(MalformedPointError) as exp:
780+
VerifyingKey.from_string(enc, valid_encodings=("hybrid", "raw"))
781+
782+
self.assertIn("(hybrid, raw)", str(exp.exception))
783+
725784
def test_decoding_with_malformed_uncompressed(self):
726785
enc = b(
727786
"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3"

0 commit comments

Comments
 (0)