Skip to content

Commit 10d7435

Browse files
authored
Merge pull request #356 from tlsfuzzer/implicit-tags
add support for parsing implicit DER tags
2 parents 55d2b56 + dba9f80 commit 10d7435

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

src/ecdsa/der.py

+69
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,32 @@ def encode_constructed(tag, value):
1616
return int2byte(0xA0 + tag) + encode_length(len(value)) + value
1717

1818

19+
def encode_implicit(tag, value, cls="context-specific"):
20+
"""
21+
Encode and IMPLICIT value using :term:`DER`.
22+
23+
:param int tag: the tag value to encode, must be between 0 an 31 inclusive
24+
:param bytes value: the data to encode
25+
:param str cls: the class of the tag to encode: "application",
26+
"context-specific", or "private"
27+
:rtype: bytes
28+
"""
29+
if cls not in ("application", "context-specific", "private"):
30+
raise ValueError("invalid tag class")
31+
if tag > 31:
32+
raise ValueError("Long tags not supported")
33+
34+
if cls == "application":
35+
tag_class = 0b01000000
36+
elif cls == "context-specific":
37+
tag_class = 0b10000000
38+
else:
39+
assert cls == "private"
40+
tag_class = 0b11000000
41+
42+
return int2byte(tag_class + tag) + encode_length(len(value)) + value
43+
44+
1945
def encode_integer(r):
2046
assert r >= 0 # can't support negative numbers yet
2147
h = ("%x" % r).encode()
@@ -142,6 +168,49 @@ def remove_constructed(string):
142168
return tag, body, rest
143169

144170

171+
def remove_implicit(string, exp_class="context-specific"):
172+
"""
173+
Removes an IMPLICIT tagged value from ``string`` following :term:`DER`.
174+
175+
:param bytes string: a byte string that can have one or more
176+
DER elements.
177+
:param str exp_class: the expected tag class of the implicitly
178+
encoded value. Possible values are: "context-specific", "application",
179+
and "private".
180+
:return: a tuple with first value being the tag without indicator bits,
181+
second being the raw bytes of the value and the third one being
182+
remaining bytes (or an empty string if there are none)
183+
:rtype: tuple(int,bytes,bytes)
184+
"""
185+
if exp_class not in ("context-specific", "application", "private"):
186+
raise ValueError("invalid `exp_class` value")
187+
if exp_class == "application":
188+
tag_class = 0b01000000
189+
elif exp_class == "context-specific":
190+
tag_class = 0b10000000
191+
else:
192+
assert exp_class == "private"
193+
tag_class = 0b11000000
194+
tag_mask = 0b11000000
195+
196+
s0 = str_idx_as_int(string, 0)
197+
198+
if (s0 & tag_mask) != tag_class:
199+
raise UnexpectedDER(
200+
"wanted class {0}, got 0x{1:02x} tag".format(exp_class, s0)
201+
)
202+
if s0 & 0b00100000 != 0:
203+
raise UnexpectedDER(
204+
"wanted type primitive, got 0x{0:02x} tag".format(s0)
205+
)
206+
207+
tag = s0 & 0x1F
208+
length, llen = read_length(string[1:])
209+
body = string[1 + llen : 1 + llen + length]
210+
rest = string[1 + llen + length :]
211+
return tag, body, rest
212+
213+
145214
def remove_sequence(string):
146215
if not string:
147216
raise UnexpectedDER("Empty string does not encode a sequence")

src/ecdsa/test_der.py

+124
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
remove_object,
2323
encode_oid,
2424
remove_constructed,
25+
remove_implicit,
2526
remove_octet_string,
2627
remove_sequence,
28+
encode_implicit,
2729
)
2830

2931

@@ -396,6 +398,128 @@ def test_with_malformed_tag(self):
396398
self.assertIn("constructed tag", str(e.exception))
397399

398400

401+
class TestRemoveImplicit(unittest.TestCase):
402+
@classmethod
403+
def setUpClass(cls):
404+
cls.exp_tag = 6
405+
cls.exp_data = b"\x0a\x0b"
406+
# data with application tag class
407+
cls.data_application = b"\x46\x02\x0a\x0b"
408+
# data with context-specific tag class
409+
cls.data_context_specific = b"\x86\x02\x0a\x0b"
410+
# data with private tag class
411+
cls.data_private = b"\xc6\x02\x0a\x0b"
412+
413+
def test_simple(self):
414+
tag, body, rest = remove_implicit(self.data_context_specific)
415+
416+
self.assertEqual(tag, self.exp_tag)
417+
self.assertEqual(body, self.exp_data)
418+
self.assertEqual(rest, b"")
419+
420+
def test_wrong_expected_class(self):
421+
with self.assertRaises(ValueError) as e:
422+
remove_implicit(self.data_context_specific, "foobar")
423+
424+
self.assertIn("invalid `exp_class` value", str(e.exception))
425+
426+
def test_with_wrong_class(self):
427+
with self.assertRaises(UnexpectedDER) as e:
428+
remove_implicit(self.data_application)
429+
430+
self.assertIn(
431+
"wanted class context-specific, got 0x46 tag", str(e.exception)
432+
)
433+
434+
def test_with_application_class(self):
435+
tag, body, rest = remove_implicit(self.data_application, "application")
436+
437+
self.assertEqual(tag, self.exp_tag)
438+
self.assertEqual(body, self.exp_data)
439+
self.assertEqual(rest, b"")
440+
441+
def test_with_private_class(self):
442+
tag, body, rest = remove_implicit(self.data_private, "private")
443+
444+
self.assertEqual(tag, self.exp_tag)
445+
self.assertEqual(body, self.exp_data)
446+
self.assertEqual(rest, b"")
447+
448+
def test_with_data_following(self):
449+
extra_data = b"\x00\x01"
450+
451+
tag, body, rest = remove_implicit(
452+
self.data_context_specific + extra_data
453+
)
454+
455+
self.assertEqual(tag, self.exp_tag)
456+
self.assertEqual(body, self.exp_data)
457+
self.assertEqual(rest, extra_data)
458+
459+
def test_with_constructed(self):
460+
data = b"\xa6\x02\x0a\x0b"
461+
462+
with self.assertRaises(UnexpectedDER) as e:
463+
remove_implicit(data)
464+
465+
self.assertIn("wanted type primitive, got 0xa6 tag", str(e.exception))
466+
467+
def test_encode_decode(self):
468+
data = b"some longish string"
469+
470+
tag, body, rest = remove_implicit(
471+
encode_implicit(6, data, "application"), "application"
472+
)
473+
474+
self.assertEqual(tag, 6)
475+
self.assertEqual(body, data)
476+
self.assertEqual(rest, b"")
477+
478+
479+
class TestEncodeImplicit(unittest.TestCase):
480+
@classmethod
481+
def setUpClass(cls):
482+
cls.data = b"\x0a\x0b"
483+
# data with application tag class
484+
cls.data_application = b"\x46\x02\x0a\x0b"
485+
# data with context-specific tag class
486+
cls.data_context_specific = b"\x86\x02\x0a\x0b"
487+
# data with private tag class
488+
cls.data_private = b"\xc6\x02\x0a\x0b"
489+
490+
def test_encode_with_default_class(self):
491+
ret = encode_implicit(6, self.data)
492+
493+
self.assertEqual(ret, self.data_context_specific)
494+
495+
def test_encode_with_application_class(self):
496+
ret = encode_implicit(6, self.data, "application")
497+
498+
self.assertEqual(ret, self.data_application)
499+
500+
def test_encode_with_context_specific_class(self):
501+
ret = encode_implicit(6, self.data, "context-specific")
502+
503+
self.assertEqual(ret, self.data_context_specific)
504+
505+
def test_encode_with_private_class(self):
506+
ret = encode_implicit(6, self.data, "private")
507+
508+
self.assertEqual(ret, self.data_private)
509+
510+
def test_encode_with_invalid_class(self):
511+
with self.assertRaises(ValueError) as e:
512+
encode_implicit(6, self.data, "foobar")
513+
514+
self.assertIn("invalid tag class", str(e.exception))
515+
516+
def test_encode_with_too_large_tag(self):
517+
with self.assertRaises(ValueError) as e:
518+
encode_implicit(32, self.data)
519+
520+
self.assertIn("Long tags not supported", str(e.exception))
521+
522+
399523
class TestRemoveOctetString(unittest.TestCase):
400524
def test_simple(self):
401525
data = b"\x04\x03\xaa\xbb\xcc"

0 commit comments

Comments
 (0)