Skip to content

Commit c5e7ac2

Browse files
authored
Merge pull request #157 from tomato42/large-oids
der: fix encoding and decoding OIDs
2 parents d6cb288 + 2dac8ee commit c5e7ac2

File tree

4 files changed

+170
-25
lines changed

4 files changed

+170
-25
lines changed

src/ecdsa/der.py

Lines changed: 23 additions & 10 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
@@ -207,9 +217,12 @@ def remove_integer(string):
207217
def read_number(string):
208218
number = 0
209219
llen = 0
210-
# base-128 big endian, with b7 set in all but the last byte
220+
if str_idx_as_int(string, 0) == 0x80:
221+
raise UnexpectedDER("Non minimal encoding of OID subidentifier")
222+
# base-128 big endian, with most significant bit set in all but the last
223+
# byte
211224
while True:
212-
if llen > len(string):
225+
if llen >= len(string):
213226
raise UnexpectedDER("ran out of length bytes")
214227
number = number << 7
215228
d = str_idx_as_int(string, llen)

src/ecdsa/test_der.py

Lines changed: 143 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,139 @@ 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_padded_first_subid(self):
315+
with self.assertRaises(UnexpectedDER):
316+
remove_object(b'\x06\x02\x80\x00')
317+
318+
def test_with_padded_second_subid(self):
319+
with self.assertRaises(UnexpectedDER):
320+
remove_object(b'\x06\x04\x88\x37\x80\x01')
321+
322+
def test_with_missing_last_byte_of_multi_byte(self):
323+
with self.assertRaises(UnexpectedDER):
324+
remove_object(b'\x06\x03\x88\x37\x83')
325+
326+
def test_with_two_subids(self):
327+
oid, rest = remove_object(b'\x06\x02\x88\x37')
328+
self.assertEqual(rest, b'')
329+
self.assertEqual(oid, (2, 999))
330+
331+
def test_zero_zero(self):
332+
oid, rest = remove_object(b'\x06\x01\x00')
333+
self.assertEqual(rest, b'')
334+
self.assertEqual(oid, (0, 0))
335+
336+
def test_empty_string(self):
337+
with self.assertRaises(UnexpectedDER):
338+
remove_object(b'')
339+
340+
def test_missing_length(self):
341+
with self.assertRaises(UnexpectedDER):
342+
remove_object(b'\x06')
343+
344+
def test_empty_oid(self):
345+
with self.assertRaises(UnexpectedDER):
346+
remove_object(b'\x06\x00')
347+
348+
def test_empty_oid_overflow(self):
349+
with self.assertRaises(UnexpectedDER):
350+
remove_object(b'\x06\x01')
351+
352+
def test_with_wrong_type(self):
353+
with self.assertRaises(UnexpectedDER):
354+
remove_object(b'\x04\x02\x88\x37')
355+
356+
def test_with_too_long_length(self):
357+
with self.assertRaises(UnexpectedDER):
358+
remove_object(b'\x06\x03\x88\x37')
359+
360+
361+
@st.composite
362+
def st_oid(draw, max_value=2**512, max_size=50):
363+
"""
364+
Hypothesis strategy that returns valid OBJECT IDENTIFIERs as tuples
365+
366+
:param max_value: maximum value of any single sub-identifier
367+
:param max_size: maximum length of the generated OID
368+
"""
369+
first = draw(st.integers(min_value=0, max_value=2))
370+
if first < 2:
371+
second = draw(st.integers(min_value=0, max_value=39))
372+
else:
373+
second = draw(st.integers(min_value=0, max_value=max_value))
374+
rest = draw(st.lists(st.integers(min_value=0, max_value=max_value),
375+
max_size=max_size))
376+
return (first, second) + tuple(rest)
377+
378+
379+
@given(st_oid())
380+
def test_oids(ids):
381+
encoded_oid = encode_oid(*ids)
382+
decoded_oid, rest = remove_object(encoded_oid)
383+
assert rest == b''
384+
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)