Skip to content

Commit 1bfb06f

Browse files
committed
der: fix encoding and decoding OIDs
make the code handle oids with a large second subidentifier (fixes #155) make the code raise correct exception when the encoded multi-byte subidentifier is missing the last byte move OID test coverage to test_der module and extend it update the OID generators in random DER generator to generate OIDs with large second subidentifier
1 parent d6cb288 commit 1bfb06f

File tree

4 files changed

+158
-24
lines changed

4 files changed

+158
-24
lines changed

src/ecdsa/der.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import binascii
44
import base64
55
import warnings
6+
from itertools import chain
67
from six import int2byte, b, text_type
78
from ._compat import str_idx_as_int
89

@@ -97,12 +98,10 @@ def encode_octet_string(s):
9798

9899

99100
def encode_oid(first, second, *pieces):
100-
assert first <= 2
101-
assert second <= 39
102-
encoded_pieces = [int2byte(40*first+second)] + [encode_number(p)
103-
for p in pieces]
104-
body = b('').join(encoded_pieces)
105-
return b('\x06') + encode_length(len(body)) + body
101+
assert 0 <= first < 2 and 0 <= second <= 39 or first == 2 and 0 <= second
102+
body = b''.join(chain([encode_number(40*first+second)],
103+
(encode_number(p) for p in pieces)))
104+
return b'\x06' + encode_length(len(body)) + body
106105

107106

108107
def encode_sequence(*encoded_pieces):
@@ -157,20 +156,31 @@ def remove_octet_string(string):
157156

158157

159158
def remove_object(string):
159+
if not string:
160+
raise UnexpectedDER(
161+
"Empty string does not encode an object identifier")
160162
if string[:1] != b"\x06":
161163
n = str_idx_as_int(string, 0)
162164
raise UnexpectedDER("wanted type 'object' (0x06), got 0x%02x" % n)
163165
length, lengthlength = read_length(string[1:])
164166
body = string[1+lengthlength:1+lengthlength+length]
165167
rest = string[1+lengthlength+length:]
168+
if not body:
169+
raise UnexpectedDER("Empty object identifier")
170+
if len(body) != length:
171+
raise UnexpectedDER(
172+
"Length of object identifier longer than the provided buffer")
166173
numbers = []
167174
while body:
168175
n, ll = read_number(body)
169176
numbers.append(n)
170177
body = body[ll:]
171178
n0 = numbers.pop(0)
172-
first = n0//40
173-
second = n0-(40*first)
179+
if n0 < 80:
180+
first = n0 // 40
181+
else:
182+
first = 2
183+
second = n0 - (40 * first)
174184
numbers.insert(0, first)
175185
numbers.insert(1, second)
176186
return tuple(numbers), rest
@@ -209,7 +219,7 @@ def read_number(string):
209219
llen = 0
210220
# base-128 big endian, with b7 set in all but the last byte
211221
while True:
212-
if llen > len(string):
222+
if llen >= len(string):
213223
raise UnexpectedDER("ran out of length bytes")
214224
number = number << 7
215225
d = str_idx_as_int(string, llen)

src/ecdsa/test_der.py

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11

22
# compatibility with Python 2.6, for that we need unittest2 package,
33
# which is not available on 3.3 or 3.4
4+
import warnings
5+
from binascii import hexlify
46
try:
57
import unittest2 as unittest
68
except ImportError:
79
import unittest
8-
from .der import remove_integer, UnexpectedDER, read_length, encode_bitstring,\
9-
remove_bitstring
1010
from six import b
11+
import hypothesis.strategies as st
12+
from hypothesis import given, example
1113
import pytest
12-
import warnings
1314
from ._compat import str_idx_as_int
15+
from .curves import NIST256p, NIST224p
16+
from .der import remove_integer, UnexpectedDER, read_length, encode_bitstring,\
17+
remove_bitstring, remove_object, encode_oid
1418

1519

1620
class TestRemoveInteger(unittest.TestCase):
@@ -242,3 +246,131 @@ def test_bytes(self):
242246

243247
def test_bytearray(self):
244248
self.assertEqual(115, str_idx_as_int(bytearray(b'str'), 0))
249+
250+
251+
class TestEncodeOid(unittest.TestCase):
252+
def test_pub_key_oid(self):
253+
oid_ecPublicKey = encode_oid(1, 2, 840, 10045, 2, 1)
254+
self.assertEqual(hexlify(oid_ecPublicKey), b("06072a8648ce3d0201"))
255+
256+
def test_nist224p_oid(self):
257+
self.assertEqual(hexlify(NIST224p.encoded_oid), b("06052b81040021"))
258+
259+
def test_nist256p_oid(self):
260+
self.assertEqual(hexlify(NIST256p.encoded_oid),
261+
b"06082a8648ce3d030107")
262+
263+
def test_large_second_subid(self):
264+
# from X.690, section 8.19.5
265+
oid = encode_oid(2, 999, 3)
266+
self.assertEqual(oid, b'\x06\x03\x88\x37\x03')
267+
268+
def test_with_two_subids(self):
269+
oid = encode_oid(2, 999)
270+
self.assertEqual(oid, b'\x06\x02\x88\x37')
271+
272+
def test_zero_zero(self):
273+
oid = encode_oid(0, 0)
274+
self.assertEqual(oid, b'\x06\x01\x00')
275+
276+
def test_with_wrong_types(self):
277+
with self.assertRaises((TypeError, AssertionError)):
278+
encode_oid(0, None)
279+
280+
def test_with_small_first_large_second(self):
281+
with self.assertRaises(AssertionError):
282+
encode_oid(1, 40)
283+
284+
def test_small_first_max_second(self):
285+
oid = encode_oid(1, 39)
286+
self.assertEqual(oid, b'\x06\x01\x4f')
287+
288+
def test_with_invalid_first(self):
289+
with self.assertRaises(AssertionError):
290+
encode_oid(3, 39)
291+
292+
293+
class TestRemoveObject(unittest.TestCase):
294+
@classmethod
295+
def setUpClass(cls):
296+
cls.oid_ecPublicKey = encode_oid(1, 2, 840, 10045, 2, 1)
297+
298+
def test_pub_key_oid(self):
299+
oid, rest = remove_object(self.oid_ecPublicKey)
300+
self.assertEqual(rest, b'')
301+
self.assertEqual(oid, (1, 2, 840, 10045, 2, 1))
302+
303+
def test_with_extra_bytes(self):
304+
oid, rest = remove_object(self.oid_ecPublicKey + b'more')
305+
self.assertEqual(rest, b'more')
306+
self.assertEqual(oid, (1, 2, 840, 10045, 2, 1))
307+
308+
def test_with_large_second_subid(self):
309+
# from X.690, section 8.19.5
310+
oid, rest = remove_object(b'\x06\x03\x88\x37\x03')
311+
self.assertEqual(rest, b'')
312+
self.assertEqual(oid, (2, 999, 3))
313+
314+
def test_with_missing_last_byte_of_multi_byte(self):
315+
with self.assertRaises(UnexpectedDER):
316+
remove_object(b'\x06\x03\x88\x37\x83')
317+
318+
def test_with_two_subids(self):
319+
oid, rest = remove_object(b'\x06\x02\x88\x37')
320+
self.assertEqual(rest, b'')
321+
self.assertEqual(oid, (2, 999))
322+
323+
def test_zero_zero(self):
324+
oid, rest = remove_object(b'\x06\x01\x00')
325+
self.assertEqual(rest, b'')
326+
self.assertEqual(oid, (0, 0))
327+
328+
def test_empty_string(self):
329+
with self.assertRaises(UnexpectedDER):
330+
remove_object(b'')
331+
332+
def test_missing_length(self):
333+
with self.assertRaises(UnexpectedDER):
334+
remove_object(b'\x06')
335+
336+
def test_empty_oid(self):
337+
with self.assertRaises(UnexpectedDER):
338+
remove_object(b'\x06\x00')
339+
340+
def test_empty_oid_overflow(self):
341+
with self.assertRaises(UnexpectedDER):
342+
remove_object(b'\x06\x01')
343+
344+
def test_with_wrong_type(self):
345+
with self.assertRaises(UnexpectedDER):
346+
remove_object(b'\x04\x02\x88\x37')
347+
348+
def test_with_too_long_length(self):
349+
with self.assertRaises(UnexpectedDER):
350+
remove_object(b'\x06\x03\x88\x37')
351+
352+
353+
@st.composite
354+
def st_oid(draw, max_value=2**512, max_size=50):
355+
"""
356+
Hypothesis strategy that returns valid OBJECT IDENTIFIERs as tuples
357+
358+
:param max_value: maximum value of any single sub-identifier
359+
:param max_size: maximum length of the generated OID
360+
"""
361+
first = draw(st.integers(min_value=0, max_value=2))
362+
if first < 2:
363+
second = draw(st.integers(min_value=0, max_value=39))
364+
else:
365+
second = draw(st.integers(min_value=0, max_value=max_value))
366+
rest = draw(st.lists(st.integers(min_value=0, max_value=max_value),
367+
max_size=max_size))
368+
return (first, second) + tuple(rest)
369+
370+
371+
@given(st_oid())
372+
def test_oids(ids):
373+
encoded_oid = encode_oid(*ids)
374+
decoded_oid, rest = remove_object(encoded_oid)
375+
assert rest == b''
376+
assert decoded_oid == ids

src/ecdsa/test_malformed_sigs.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,10 @@ def st_der_oid(draw):
220220
Hypothesis strategy that returns DER OBJECT IDENTIFIER objects.
221221
"""
222222
first = draw(st.integers(min_value=0, max_value=2))
223-
second = draw(st.integers(min_value=0, max_value=39))
223+
if first < 2:
224+
second = draw(st.integers(min_value=0, max_value=39))
225+
else:
226+
second = draw(st.integers(min_value=0, max_value=2**512))
224227
rest = draw(st.lists(st.integers(min_value=0, max_value=2**512),
225228
max_size=50))
226229
return encode_oid(first, second, *rest)

src/ecdsa/test_pyecdsa.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -952,17 +952,6 @@ def do_test_to_openssl(self, curve):
952952

953953

954954
class DER(unittest.TestCase):
955-
def test_oids(self):
956-
oid_ecPublicKey = der.encode_oid(1, 2, 840, 10045, 2, 1)
957-
self.assertEqual(hexlify(oid_ecPublicKey), b("06072a8648ce3d0201"))
958-
self.assertEqual(hexlify(NIST224p.encoded_oid), b("06052b81040021"))
959-
self.assertEqual(hexlify(NIST256p.encoded_oid),
960-
b("06082a8648ce3d030107"))
961-
x = oid_ecPublicKey + b("more")
962-
x1, rest = der.remove_object(x)
963-
self.assertEqual(x1, (1, 2, 840, 10045, 2, 1))
964-
self.assertEqual(rest, b("more"))
965-
966955
def test_integer(self):
967956
self.assertEqual(der.encode_integer(0), b("\x02\x01\x00"))
968957
self.assertEqual(der.encode_integer(1), b("\x02\x01\x01"))

0 commit comments

Comments
 (0)