Skip to content

Commit a13b1d0

Browse files
committed
allow limiting point formats, don't accept malformed PEM public files
Allow specifying what point formats are supported when loading public keys. Limit the point formats when loading PEM and DER public files to the formats actually allowed in them: uncompressed, compressed, and hybrid. Previous code would allow raw encoding too.
1 parent 4e059b9 commit a13b1d0

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)