Skip to content

Commit 4984db1

Browse files
committed
Adds to DER format of ECDSA signature with extended ECDSA-Sig-Value and ECDSA-Full-R.
1 parent d547d92 commit 4984db1

File tree

8 files changed

+883
-20
lines changed

8 files changed

+883
-20
lines changed

src/ecdsa/der.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ def encode_number(n):
151151
return b"".join([int2byte(d) for d in b128_digits])
152152

153153

154+
def encode_boolean(b):
155+
"""
156+
Encodes BOOLEAN acording to ASN.1 DER format.
157+
The ASN.1 BOOLEAN type has two possible values: TRUE and FALSE.
158+
True is encoded as ff", False is encoded as a zero.
159+
160+
:param boolean b: the boolean value to be encoded
161+
:return: a byte string
162+
:rtype: bytes
163+
"""
164+
165+
return b"\x01" + encode_length(1) + (b"\xff" if b else b"\x00")
166+
167+
154168
def is_sequence(string):
155169
return string and string[:1] == b"\x30"
156170

@@ -240,6 +254,45 @@ def remove_octet_string(string):
240254
return body, rest
241255

242256

257+
def remove_boolean(string):
258+
"""
259+
Removes the ASN.1 BOOLEAN type.
260+
For BOOLEAN types, in DER FALSE is always encoded as zero
261+
and TRUE is always encoded as ff.
262+
263+
:param bytes string: the boolean value to be encoded
264+
:return: a boolean value and the rest of the string
265+
:rtype: tuple(boolean, bytes)
266+
"""
267+
if not string:
268+
raise UnexpectedDER("Empty string is an invalid encoding of a boolean")
269+
if string[:1] != b"\x01":
270+
n = str_idx_as_int(string, 0)
271+
raise UnexpectedDER("wanted type 'boolean' (0x01), got 0x%02x" % n)
272+
length, lengthlength = read_length(string[1:])
273+
if not length:
274+
raise UnexpectedDER("Invalid length of bit string, can't be 0")
275+
body = string[1 + lengthlength : 1 + lengthlength + length]
276+
rest = string[1 + lengthlength + length :]
277+
if not body:
278+
raise UnexpectedDER("Empty BOOLEAN value")
279+
if length != 1:
280+
raise UnexpectedDER(
281+
"The contents octets of boolean shall consist of a single octet."
282+
)
283+
if body == b"\x00":
284+
return False, rest
285+
# the workaround due to instrumental, that
286+
# saves the binary data as UTF-8 string
287+
# (0xff is an invalid start byte)
288+
if isinstance(body, text_type):
289+
body = body.encode("utf-8")
290+
num = int(binascii.hexlify(body), 16)
291+
if num == 0xFF:
292+
return True, rest
293+
raise UnexpectedDER("Invalid encoding of BOOLEAN.")
294+
295+
243296
def remove_object(string):
244297
if not string:
245298
raise UnexpectedDER(
@@ -298,8 +351,7 @@ def remove_integer(string):
298351
smsb = str_idx_as_int(numberbytes, 1)
299352
if smsb < 0x80:
300353
raise UnexpectedDER(
301-
"Invalid encoding of integer, unnecessary "
302-
"zero padding bytes"
354+
"Invalid encoding of integer, unnecessary zero padding bytes"
303355
)
304356
return int(binascii.hexlify(numberbytes), 16), rest
305357

@@ -399,8 +451,8 @@ def remove_bitstring(string, expect_unused=_sentry):
399451
raise UnexpectedDER("Empty string does not encode a bitstring")
400452
if expect_unused is _sentry:
401453
warnings.warn(
402-
"Legacy call convention used, expect_unused= needs to be"
403-
" specified",
454+
"Legacy call convention used, "
455+
"expect_unused= needs to be specified",
404456
DeprecationWarning,
405457
)
406458
num = str_idx_as_int(string, 0)

src/ecdsa/ecdsa.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
modified as part of the python-ecdsa package.
6565
"""
6666

67+
import sys
6768
import warnings
6869
from six import int2byte
6970
from . import ellipticcurve
@@ -192,10 +193,22 @@ def verifies(self, hash, signature):
192193
n = G.order()
193194
r = signature.r
194195
s = signature.s
195-
if r < 1 or r > n - 1:
196-
return False
197196
if s < 1 or s > n - 1:
198197
return False
198+
199+
if sys.version_info < (3, 0): # pragma: no branch
200+
# memoryview was introduced in py 2.7
201+
byte_objects = set((bytearray, bytes))
202+
else:
203+
byte_objects = set((bytearray, bytes, memoryview))
204+
if type(r) in byte_objects:
205+
point = ellipticcurve.AbstractPoint.from_bytes(
206+
self.generator.curve(), r
207+
)
208+
r = point[0] % n
209+
210+
if r < 1 or r > n - 1:
211+
return False
199212
c = numbertheory.inverse_mod(s, n)
200213
u1 = (hash * c) % n
201214
u2 = (r * c) % n
@@ -231,7 +244,7 @@ def __ne__(self, other):
231244
"""Return False if the points are identical, True otherwise."""
232245
return not self == other
233246

234-
def sign(self, hash, random_k):
247+
def sign(self, hash, random_k, accelerate=False):
235248
"""Return a signature for the provided hash, using the provided
236249
random nonce. It is absolutely vital that random_k be an unpredictable
237250
number in the range [1, self.public_key.point.order()-1]. If
@@ -267,6 +280,8 @@ def sign(self, hash, random_k):
267280
) % n
268281
if s == 0:
269282
raise RSZeroError("amazingly unlucky random number s")
283+
if accelerate:
284+
return Signature(p1, s)
270285
return Signature(r, s)
271286

272287

src/ecdsa/keys.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,7 @@ def sign_deterministic(
13181318
hashfunc=None,
13191319
sigencode=sigencode_string,
13201320
extra_entropy=b"",
1321+
accelerate=False,
13211322
):
13221323
"""
13231324
Create signature over data.
@@ -1354,6 +1355,10 @@ def sign_deterministic(
13541355
number generator used in the RFC6979 process. Entirely optional.
13551356
Ignored with EdDSA.
13561357
:type extra_entropy: :term:`bytes-like object`
1358+
:param accelerate: an indicator for ECDSA sign operation to return
1359+
an ECPoint instead of a number of "r" parameter.
1360+
Applicable only for ECDSA key.
1361+
:type accelerate: boolean
13571362
13581363
:return: encoded signature over `data`
13591364
:rtype: bytes or sigencode function dependent type
@@ -1373,6 +1378,7 @@ def sign_deterministic(
13731378
sigencode=sigencode,
13741379
extra_entropy=extra_entropy,
13751380
allow_truncate=True,
1381+
accelerate=accelerate,
13761382
)
13771383

13781384
def sign_digest_deterministic(
@@ -1382,6 +1388,7 @@ def sign_digest_deterministic(
13821388
sigencode=sigencode_string,
13831389
extra_entropy=b"",
13841390
allow_truncate=False,
1391+
accelerate=False,
13851392
):
13861393
"""
13871394
Create signature for digest using the deterministic RFC6979 algorithm.
@@ -1417,6 +1424,10 @@ def sign_digest_deterministic(
14171424
bigger bit-size than the order of the curve, the extra bits (at
14181425
the end of the digest) will be truncated. Use it when signing
14191426
SHA-384 output using NIST256p or in similar situations.
1427+
:param accelerate: an indicator for ECDSA sign operation to return
1428+
an ECPoint instead of a number of "r" parameter.
1429+
Applicable only for ECDSA key.
1430+
:type accelerate: boolean
14201431
14211432
:return: encoded signature for the `digest` hash
14221433
:rtype: bytes or sigencode function dependent type
@@ -1447,6 +1458,7 @@ def simple_r_s(r, s, order):
14471458
sigencode=simple_r_s,
14481459
k=k,
14491460
allow_truncate=allow_truncate,
1461+
accelerate=accelerate,
14501462
)
14511463
break
14521464
except RSZeroError:
@@ -1462,6 +1474,7 @@ def sign(
14621474
sigencode=sigencode_string,
14631475
k=None,
14641476
allow_truncate=True,
1477+
accelerate=False,
14651478
):
14661479
"""
14671480
Create signature over data.
@@ -1525,6 +1538,10 @@ def sign(
15251538
leak the key. Caller should try a better entropy source, retry with
15261539
different ``k``, or use the
15271540
:func:`~SigningKey.sign_deterministic` in such case.
1541+
:param accelerate: an indicator for ECDSA sign operation to return
1542+
an ECPoint instead of a number of "r" parameter.
1543+
Applicable only for ECDSA key.
1544+
:type accelerate: boolean
15281545
15291546
:return: encoded signature of the hash of `data`
15301547
:rtype: bytes or sigencode function dependent type
@@ -1534,7 +1551,9 @@ def sign(
15341551
if isinstance(self.curve.curve, CurveEdTw):
15351552
return self.sign_deterministic(data)
15361553
h = hashfunc(data).digest()
1537-
return self.sign_digest(h, entropy, sigencode, k, allow_truncate)
1554+
return self.sign_digest(
1555+
h, entropy, sigencode, k, allow_truncate, accelerate
1556+
)
15381557

15391558
def sign_digest(
15401559
self,
@@ -1543,6 +1562,7 @@ def sign_digest(
15431562
sigencode=sigencode_string,
15441563
k=None,
15451564
allow_truncate=False,
1565+
accelerate=False,
15461566
):
15471567
"""
15481568
Create signature over digest using the probabilistic ECDSA algorithm.
@@ -1579,6 +1599,10 @@ def sign_digest(
15791599
leak the key. Caller should try a better entropy source, retry with
15801600
different 'k', or use the
15811601
:func:`~SigningKey.sign_digest_deterministic` in such case.
1602+
:param accelerate: an indicator for ECDSA sign operation to return
1603+
an ECPoint instead of a number of "r" parameter.
1604+
Applicable only for ECDSA key.
1605+
:type accelerate: boolean
15821606
15831607
:return: encoded signature for the `digest` hash
15841608
:rtype: bytes or sigencode function dependent type
@@ -1591,10 +1615,10 @@ def sign_digest(
15911615
self.curve,
15921616
allow_truncate,
15931617
)
1594-
r, s = self.sign_number(number, entropy, k)
1618+
r, s = self.sign_number(number, entropy, k, accelerate)
15951619
return sigencode(r, s, self.privkey.order)
15961620

1597-
def sign_number(self, number, entropy=None, k=None):
1621+
def sign_number(self, number, entropy=None, k=None, accelerate=False):
15981622
"""
15991623
Sign an integer directly.
16001624
@@ -1613,6 +1637,10 @@ def sign_number(self, number, entropy=None, k=None):
16131637
leak the key. Caller should try a better entropy source, retry with
16141638
different 'k', or use the
16151639
:func:`~SigningKey.sign_digest_deterministic` in such case.
1640+
:param accelerate: an indicator for ECDSA sign operation to return
1641+
an ECPoint instead of a number of "r" parameter.
1642+
Applicable only for ECDSA key.
1643+
:type accelerate: boolean
16161644
16171645
:return: the "r" and "s" parameters of the signature
16181646
:rtype: tuple of ints
@@ -1627,5 +1655,5 @@ def sign_number(self, number, entropy=None, k=None):
16271655
_k = randrange(order, entropy)
16281656

16291657
assert 1 <= _k < order
1630-
sig = self.privkey.sign(number, _k)
1658+
sig = self.privkey.sign(number, _k, accelerate)
16311659
return sig.r, sig.s

src/ecdsa/test_der.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from ._compat import str_idx_as_int
1515
from .curves import NIST256p, NIST224p
1616
from .der import (
17+
remove_boolean,
1718
remove_integer,
1819
UnexpectedDER,
1920
read_length,
@@ -26,6 +27,7 @@
2627
remove_octet_string,
2728
remove_sequence,
2829
encode_implicit,
30+
encode_boolean,
2931
)
3032

3133

@@ -565,6 +567,82 @@ def test_with_wrong_length(self):
565567
self.assertIn("Length longer", str(e.exception))
566568

567569

570+
class TestEncodeBoolean(unittest.TestCase):
571+
def test_simple_true(self):
572+
der = encode_boolean(True)
573+
self.assertEqual(len(der), 3)
574+
self.assertEqual(der, b"\x01\x01\xff")
575+
576+
def test_simle_false(self):
577+
der = encode_boolean(False)
578+
self.assertEqual(len(der), 3)
579+
self.assertEqual(der, b"\x01\x01\x00")
580+
581+
582+
class TestRemoveBoolean(unittest.TestCase):
583+
def test_simple_false(self):
584+
data = b"\x01\x01\x00"
585+
body, rest = remove_boolean(data)
586+
self.assertEqual(body, False)
587+
self.assertEqual(rest, b"")
588+
589+
def test_simple_true(self):
590+
data = b"\x01\x01\xff"
591+
body, rest = remove_boolean(data)
592+
self.assertEqual(body, True)
593+
self.assertEqual(rest, b"")
594+
595+
def test_empty_string(self):
596+
with self.assertRaises(UnexpectedDER) as e:
597+
remove_boolean(b"")
598+
599+
def test_with_wrong_tag(self):
600+
data = b"\x02\x01\x00"
601+
602+
with self.assertRaises(UnexpectedDER) as e:
603+
remove_boolean(data)
604+
605+
self.assertIn("wanted type 'boolean' (0x01)", str(e.exception))
606+
607+
def test_with_wrong_encoded_value(self):
608+
data = b"\x01\x01\x01"
609+
610+
with self.assertRaises(UnexpectedDER) as e:
611+
remove_boolean(data)
612+
613+
self.assertIn("Invalid encoding of BOOLEAN", str(e.exception))
614+
615+
def test_empty_body(self):
616+
data = b"\x01\x01"
617+
618+
with self.assertRaises(UnexpectedDER) as e:
619+
remove_boolean(data)
620+
621+
self.assertIn("Empty BOOLEAN value", str(e.exception))
622+
623+
def test_several_boolean_octets(self):
624+
data = b"\x01\x02\x01\x01"
625+
626+
with self.assertRaises(UnexpectedDER) as e:
627+
remove_boolean(data)
628+
629+
self.assertIn(
630+
"The contents octets of boolean "
631+
"shall consist of a single octet",
632+
str(e.exception),
633+
)
634+
635+
def test_zero_boolean_length(self):
636+
data = b"\x01\x00"
637+
638+
with self.assertRaises(UnexpectedDER) as e:
639+
remove_boolean(data)
640+
641+
self.assertIn(
642+
"Invalid length of bit string, can't be 0", str(e.exception)
643+
)
644+
645+
568646
@st.composite
569647
def st_oid(draw, max_value=2**512, max_size=50):
570648
"""
@@ -614,7 +692,7 @@ def test_remove_octet_string_rejects_truncated_length():
614692
def test_remove_constructed_rejects_truncated_length():
615693
# Constructed tag: 0xA0 (context-specific constructed, tag=0)
616694
# declared length 4096, but only 3 bytes present
617-
bad = b"\xA0\x82\x10\x00" + b"ABC"
695+
bad = b"\xa0\x82\x10\x00" + b"ABC"
618696
with pytest.raises(
619697
UnexpectedDER, match="Length longer than the provided buffer"
620698
):

src/ecdsa/test_keys.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,7 @@ def test_SigningKey_from_string(convert):
10371037
key_bytes = unpem(prv_key_str)
10381038
assert isinstance(key_bytes, bytes)
10391039

1040+
10401041
# last two converters are for array.array of ints, those require input
10411042
# that's multiple of 4, which no curve we support produces
10421043
@pytest.mark.parametrize("convert", converters[:-2])

0 commit comments

Comments
 (0)