From 8e3f653e66150b9c787d6ca4fb015d05dd164e51 Mon Sep 17 00:00:00 2001 From: Alicja Kario Date: Thu, 6 Mar 2025 18:27:37 +0100 Subject: [PATCH 1/2] add support for parsing implicit DER tags --- src/ecdsa/der.py | 43 +++++++++++++++++++++++++++ src/ecdsa/test_der.py | 68 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/src/ecdsa/der.py b/src/ecdsa/der.py index b2914859..be61b867 100644 --- a/src/ecdsa/der.py +++ b/src/ecdsa/der.py @@ -142,6 +142,49 @@ def remove_constructed(string): return tag, body, rest +def remove_implicit(string, exp_class="context-specific"): + """ + Removes an IMPLICIT tagged value from ``string`` following :term:`DER`. + + :param bytes string: a byte string that can have one or more + DER elements. + :param str exp_class: the expected tag class of the implicitly + encoded value. Possible values are: "context-specific", "application", + and "private". + :return: a tuple with first value being the tag without indicator bits, + second being the raw bytes of the value and the third one being + remaining bytes (or an empty string if there are none) + :rtype: tuple(int,bytes,bytes) + """ + if exp_class not in ("context-specific", "application", "private"): + raise ValueError("invalid `exp_class` value") + if exp_class == "application": + tag_class = 0b01000000 + elif exp_class == "context-specific": + tag_class = 0b10000000 + else: + assert exp_class == "private" + tag_class = 0b11000000 + tag_mask = 0b11000000 + + s0 = str_idx_as_int(string, 0) + + if (s0 & tag_mask) != tag_class: + raise UnexpectedDER( + "wanted class {0}, got 0x{1:02x} tag".format(exp_class, s0) + ) + if s0 & 0b00100000 != 0: + raise UnexpectedDER( + "wanted type primitive, got 0x{0:02x} tag".format(s0) + ) + + tag = s0 & 0x1F + length, llen = read_length(string[1:]) + body = string[1 + llen : 1 + llen + length] + rest = string[1 + llen + length :] + return tag, body, rest + + def remove_sequence(string): if not string: raise UnexpectedDER("Empty string does not encode a sequence") diff --git a/src/ecdsa/test_der.py b/src/ecdsa/test_der.py index 0c2dc4d1..87eae1e8 100644 --- a/src/ecdsa/test_der.py +++ b/src/ecdsa/test_der.py @@ -22,6 +22,7 @@ remove_object, encode_oid, remove_constructed, + remove_implicit, remove_octet_string, remove_sequence, ) @@ -396,6 +397,73 @@ def test_with_malformed_tag(self): self.assertIn("constructed tag", str(e.exception)) +class TestRemoveImplicit(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.exp_tag = 6 + cls.exp_data = b"\x0a\x0b" + # data with application tag class + cls.data_application = b"\x46\x02\x0a\x0b" + # data with context-specific tag class + cls.data_context_specific = b"\x86\x02\x0a\x0b" + # data with private tag class + cls.data_private = b"\xc6\x02\x0a\x0b" + + def test_simple(self): + tag, body, rest = remove_implicit(self.data_context_specific) + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, b"") + + def test_wrong_expected_class(self): + with self.assertRaises(ValueError) as e: + remove_implicit(self.data_context_specific, "foobar") + + self.assertIn("invalid `exp_class` value", str(e.exception)) + + def test_with_wrong_class(self): + with self.assertRaises(UnexpectedDER) as e: + remove_implicit(self.data_application) + + self.assertIn( + "wanted class context-specific, got 0x46 tag", str(e.exception) + ) + + def test_with_application_class(self): + tag, body, rest = remove_implicit(self.data_application, "application") + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, b"") + + def test_with_private_class(self): + tag, body, rest = remove_implicit(self.data_private, "private") + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, b"") + + def test_with_data_following(self): + extra_data = b"\x00\x01" + + tag, body, rest = remove_implicit( + self.data_context_specific + extra_data + ) + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, extra_data) + + def test_with_constructed(self): + data = b"\xa6\x02\x0a\x0b" + + with self.assertRaises(UnexpectedDER) as e: + remove_implicit(data) + + self.assertIn("wanted type primitive, got 0xa6 tag", str(e.exception)) + + class TestRemoveOctetString(unittest.TestCase): def test_simple(self): data = b"\x04\x03\xaa\xbb\xcc" From dba9f8096268f7e97ff1f7a21c910fff4d764e6d Mon Sep 17 00:00:00 2001 From: Alicja Kario Date: Thu, 6 Mar 2025 18:55:31 +0100 Subject: [PATCH 2/2] add support for encoding --- src/ecdsa/der.py | 26 ++++++++++++++++++++ src/ecdsa/test_der.py | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/ecdsa/der.py b/src/ecdsa/der.py index be61b867..7a06b681 100644 --- a/src/ecdsa/der.py +++ b/src/ecdsa/der.py @@ -16,6 +16,32 @@ def encode_constructed(tag, value): return int2byte(0xA0 + tag) + encode_length(len(value)) + value +def encode_implicit(tag, value, cls="context-specific"): + """ + Encode and IMPLICIT value using :term:`DER`. + + :param int tag: the tag value to encode, must be between 0 an 31 inclusive + :param bytes value: the data to encode + :param str cls: the class of the tag to encode: "application", + "context-specific", or "private" + :rtype: bytes + """ + if cls not in ("application", "context-specific", "private"): + raise ValueError("invalid tag class") + if tag > 31: + raise ValueError("Long tags not supported") + + if cls == "application": + tag_class = 0b01000000 + elif cls == "context-specific": + tag_class = 0b10000000 + else: + assert cls == "private" + tag_class = 0b11000000 + + return int2byte(tag_class + tag) + encode_length(len(value)) + value + + def encode_integer(r): assert r >= 0 # can't support negative numbers yet h = ("%x" % r).encode() diff --git a/src/ecdsa/test_der.py b/src/ecdsa/test_der.py index 87eae1e8..b0955431 100644 --- a/src/ecdsa/test_der.py +++ b/src/ecdsa/test_der.py @@ -25,6 +25,7 @@ remove_implicit, remove_octet_string, remove_sequence, + encode_implicit, ) @@ -463,6 +464,61 @@ def test_with_constructed(self): self.assertIn("wanted type primitive, got 0xa6 tag", str(e.exception)) + def test_encode_decode(self): + data = b"some longish string" + + tag, body, rest = remove_implicit( + encode_implicit(6, data, "application"), "application" + ) + + self.assertEqual(tag, 6) + self.assertEqual(body, data) + self.assertEqual(rest, b"") + + +class TestEncodeImplicit(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.data = b"\x0a\x0b" + # data with application tag class + cls.data_application = b"\x46\x02\x0a\x0b" + # data with context-specific tag class + cls.data_context_specific = b"\x86\x02\x0a\x0b" + # data with private tag class + cls.data_private = b"\xc6\x02\x0a\x0b" + + def test_encode_with_default_class(self): + ret = encode_implicit(6, self.data) + + self.assertEqual(ret, self.data_context_specific) + + def test_encode_with_application_class(self): + ret = encode_implicit(6, self.data, "application") + + self.assertEqual(ret, self.data_application) + + def test_encode_with_context_specific_class(self): + ret = encode_implicit(6, self.data, "context-specific") + + self.assertEqual(ret, self.data_context_specific) + + def test_encode_with_private_class(self): + ret = encode_implicit(6, self.data, "private") + + self.assertEqual(ret, self.data_private) + + def test_encode_with_invalid_class(self): + with self.assertRaises(ValueError) as e: + encode_implicit(6, self.data, "foobar") + + self.assertIn("invalid tag class", str(e.exception)) + + def test_encode_with_too_large_tag(self): + with self.assertRaises(ValueError) as e: + encode_implicit(32, self.data) + + self.assertIn("Long tags not supported", str(e.exception)) + class TestRemoveOctetString(unittest.TestCase): def test_simple(self):