Skip to content

Commit 64ccb34

Browse files
author
Lostboi
committed
Merged etingof#196 from etingof/pyasn1
1 parent d07dd2e commit 64ccb34

10 files changed

Lines changed: 394 additions & 0 deletions

File tree

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Revision 0.5.0, released XX-03-2020
2222
`StreamingDecoder` class. Previously published API is implemented
2323
as a thin wrapper on top of that ensuring backward compatibility.
2424

25+
- Added support for previously missing `RELATIVE-OID` construct.
26+
2527
Revision 0.4.9, released XX-03-2020
2628
-----------------------------------
2729

THANKS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ Alex Gaynor
88
Geoffrey Thomas
99
Daniel Bratell
1010
Kim Gräsman
11+
Russ Housley

pyasn1/codec/ber/decoder.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,54 @@ def valueDecoder(self, substrate, asn1Spec,
147147

148148
yield self._createComponent(asn1Spec, tagSet, value, **options)
149149

150+
class RelativeOIDPayloadDecoder(AbstractSimplePayloadDecoder):
151+
protoComponent = univ.RelativeOID(())
152+
153+
def valueDecoder(self, substrate, asn1Spec,
154+
tagSet=None, length=None, state=None,
155+
decodeFun=None, substrateFun=None,
156+
**options):
157+
if tagSet[0].tagFormat != tag.tagFormatSimple:
158+
raise error.PyAsn1Error('Simple tag format expected')
159+
160+
for chunk in readFromStream(substrate, length, options):
161+
if isinstance(chunk, SubstrateUnderrunError):
162+
yield chunk
163+
164+
if not chunk:
165+
raise error.PyAsn1Error('Empty substrate')
166+
167+
chunk = octs2ints(chunk)
168+
169+
reloid = ()
170+
index = 0
171+
substrateLen = len(chunk)
172+
while index < substrateLen:
173+
subId = chunk[index]
174+
index += 1
175+
if subId < 128:
176+
reloid += (subId,)
177+
elif subId > 128:
178+
# Construct subid from a number of octets
179+
nextSubId = subId
180+
subId = 0
181+
while nextSubId >= 128:
182+
subId = (subId << 7) + (nextSubId & 0x7F)
183+
if index >= substrateLen:
184+
raise error.SubstrateUnderrunError(
185+
'Short substrate for sub-OID past %s' % (reloid,)
186+
)
187+
nextSubId = chunk[index]
188+
index += 1
189+
reloid += ((subId << 7) + nextSubId,)
190+
elif subId == 128:
191+
# ASN.1 spec forbids leading zeros (0x80) in OID
192+
# encoding, tolerating it opens a vulnerability. See
193+
# https://www.esat.kuleuven.be/cosic/publications/article-1432.pdf
194+
# page 7
195+
raise error.PyAsn1Error('Invalid octet 0x80 in RELATIVE-OID encoding')
196+
197+
yield self._createComponent(asn1Spec, tagSet, reloid, **options)
150198

151199
class BooleanPayloadDecoder(IntegerPayloadDecoder):
152200
protoComponent = univ.Boolean(0)
@@ -1417,6 +1465,7 @@ class UTCTimePayloadDecoder(OctetStringPayloadDecoder):
14171465
univ.OctetString.tagSet: OctetStringPayloadDecoder(),
14181466
univ.Null.tagSet: NullPayloadDecoder(),
14191467
univ.ObjectIdentifier.tagSet: ObjectIdentifierPayloadDecoder(),
1468+
univ.RelativeOID.tagSet: RelativeOIDPayloadDecoder(),
14201469
univ.Enumerated.tagSet: IntegerPayloadDecoder(),
14211470
univ.Real.tagSet: RealPayloadDecoder(),
14221471
univ.Sequence.tagSet: SequenceOrSequenceOfPayloadDecoder(), # conflicts with SequenceOf

pyasn1/codec/ber/encoder.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,39 @@ def encodeValue(self, value, asn1Spec, encodeFun, **options):
352352
return octets, False, False
353353

354354

355+
class RelativeOIDEncoder(AbstractItemEncoder):
356+
supportIndefLenMode = False
357+
358+
def encodeValue(self, value, asn1Spec, encodeFun, **options):
359+
if asn1Spec is not None:
360+
value = asn1Spec.clone(value)
361+
362+
octets = ()
363+
364+
# Cycle through subIds
365+
for subOid in value.asTuple():
366+
if 0 <= subOid <= 127:
367+
# Optimize for the common case
368+
octets += (subOid,)
369+
370+
elif subOid > 127:
371+
# Pack large Sub-Object IDs
372+
res = (subOid & 0x7f,)
373+
subOid >>= 7
374+
375+
while subOid:
376+
res = (0x80 | (subOid & 0x7f),) + res
377+
subOid >>= 7
378+
379+
# Add packed Sub-Object ID to resulted RELATIVE-OID
380+
octets += res
381+
382+
else:
383+
raise error.PyAsn1Error('Negative RELATIVE-OID arc %s at %s' % (subOid, value))
384+
385+
return octets, False, False
386+
387+
355388
class RealEncoder(AbstractItemEncoder):
356389
supportIndefLenMode = 0
357390
binEncBase = 2 # set to None to choose encoding base automatically
@@ -714,6 +747,7 @@ def encodeValue(self, value, asn1Spec, encodeFun, **options):
714747
univ.OctetString.tagSet: OctetStringEncoder(),
715748
univ.Null.tagSet: NullEncoder(),
716749
univ.ObjectIdentifier.tagSet: ObjectIdentifierEncoder(),
750+
univ.RelativeOID.tagSet: RelativeOIDEncoder(),
717751
univ.Enumerated.tagSet: IntegerEncoder(),
718752
univ.Real.tagSet: RealEncoder(),
719753
# Sequence & Set have same tags as SequenceOf & SetOf
@@ -746,6 +780,7 @@ def encodeValue(self, value, asn1Spec, encodeFun, **options):
746780
univ.OctetString.typeId: OctetStringEncoder(),
747781
univ.Null.typeId: NullEncoder(),
748782
univ.ObjectIdentifier.typeId: ObjectIdentifierEncoder(),
783+
univ.RelativeOID.typeId: RelativeOIDEncoder(),
749784
univ.Enumerated.typeId: IntegerEncoder(),
750785
univ.Real.typeId: RealEncoder(),
751786
# Sequence & Set have same tags as SequenceOf & SetOf

pyasn1/codec/native/decoder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __call__(self, pyObject, asn1Spec, decodeFun=None, **options):
6969
univ.Boolean.tagSet: AbstractScalarPayloadDecoder(),
7070
univ.BitString.tagSet: BitStringPayloadDecoder(),
7171
univ.OctetString.tagSet: AbstractScalarPayloadDecoder(),
72+
univ.RelativeOID.tagSet: AbstractScalarPayloadDecoder(),
7273
univ.Null.tagSet: AbstractScalarPayloadDecoder(),
7374
univ.ObjectIdentifier.tagSet: AbstractScalarPayloadDecoder(),
7475
univ.Enumerated.tagSet: AbstractScalarPayloadDecoder(),
@@ -102,6 +103,7 @@ def __call__(self, pyObject, asn1Spec, decodeFun=None, **options):
102103
univ.OctetString.typeId: AbstractScalarPayloadDecoder(),
103104
univ.Null.typeId: AbstractScalarPayloadDecoder(),
104105
univ.ObjectIdentifier.typeId: AbstractScalarPayloadDecoder(),
106+
univ.RelativeOID.typeId: AbstractScalarPayloadDecoder(),
105107
univ.Enumerated.typeId: AbstractScalarPayloadDecoder(),
106108
univ.Real.typeId: AbstractScalarPayloadDecoder(),
107109
# ambiguous base types

pyasn1/codec/native/encoder.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ def encode(self, value, encodeFun, **options):
5959
return str(value)
6060

6161

62+
class RelativeOIDEncoder(AbstractItemEncoder):
63+
def encode(self, value, encodeFun, **options):
64+
return str(value)
65+
66+
6267
class RealEncoder(AbstractItemEncoder):
6368
def encode(self, value, encodeFun, **options):
6469
return float(value)
@@ -108,6 +113,7 @@ def encode(self, value, encodeFun, **options):
108113
univ.Integer.tagSet: IntegerEncoder(),
109114
univ.BitString.tagSet: BitStringEncoder(),
110115
univ.OctetString.tagSet: OctetStringEncoder(),
116+
univ.RelativeOID.tagSet: RelativeOIDEncoder(),
111117
univ.Null.tagSet: NullEncoder(),
112118
univ.ObjectIdentifier.tagSet: ObjectIdentifierEncoder(),
113119
univ.Enumerated.tagSet: IntegerEncoder(),

pyasn1/type/univ.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,134 @@ def prettyOut(self, value):
12441244
return '.'.join([str(x) for x in value])
12451245

12461246

1247+
class RelativeOID(base.SimpleAsn1Type):
1248+
"""Create |ASN.1| schema or value object.
1249+
|ASN.1| class is based on :class:`~pyasn1.type.base.SimpleAsn1Type`, its
1250+
objects are immutable and duck-type Python :class:`tuple` objects
1251+
(tuple of non-negative integers).
1252+
Keyword Args
1253+
------------
1254+
value: :class:`tuple`, :class:`str` or |ASN.1| object
1255+
Python sequence of :class:`int` or :class:`str` literal or |ASN.1| object.
1256+
If `value` is not given, schema object will be created.
1257+
tagSet: :py:class:`~pyasn1.type.tag.TagSet`
1258+
Object representing non-default ASN.1 tag(s)
1259+
subtypeSpec: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection`
1260+
Object representing non-default ASN.1 subtype constraint(s). Constraints
1261+
verification for |ASN.1| type occurs automatically on object
1262+
instantiation.
1263+
Raises
1264+
------
1265+
~pyasn1.error.ValueConstraintError, ~pyasn1.error.PyAsn1Error
1266+
On constraint violation or bad initializer.
1267+
Examples
1268+
--------
1269+
.. code-block:: python
1270+
class RelOID(RelativeOID):
1271+
'''
1272+
ASN.1 specification:
1273+
id-pad-null RELATIVE-OID ::= { 0 }
1274+
id-pad-once RELATIVE-OID ::= { 5 6 }
1275+
id-pad-twice RELATIVE-OID ::= { 5 6 7 }
1276+
'''
1277+
id_pad_null = RelOID('0')
1278+
id_pad_once = RelOID('5.6')
1279+
id_pad_twice = id_pad_once + (7,)
1280+
"""
1281+
#: Set (on class, not on instance) or return a
1282+
#: :py:class:`~pyasn1.type.tag.TagSet` object representing ASN.1 tag(s)
1283+
#: associated with |ASN.1| type.
1284+
tagSet = tag.initTagSet(
1285+
tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x0d)
1286+
)
1287+
1288+
#: Set (on class, not on instance) or return a
1289+
#: :py:class:`~pyasn1.type.constraint.ConstraintsIntersection` object
1290+
#: imposing constraints on |ASN.1| type initialization values.
1291+
subtypeSpec = constraint.ConstraintsIntersection()
1292+
1293+
# Optimization for faster codec lookup
1294+
typeId = base.SimpleAsn1Type.getTypeId()
1295+
1296+
def __add__(self, other):
1297+
return self.clone(self._value + other)
1298+
1299+
def __radd__(self, other):
1300+
return self.clone(other + self._value)
1301+
1302+
def asTuple(self):
1303+
return self._value
1304+
1305+
# Sequence object protocol
1306+
1307+
def __len__(self):
1308+
return len(self._value)
1309+
1310+
def __getitem__(self, i):
1311+
if i.__class__ is slice:
1312+
return self.clone(self._value[i])
1313+
else:
1314+
return self._value[i]
1315+
1316+
def __iter__(self):
1317+
return iter(self._value)
1318+
1319+
def __contains__(self, value):
1320+
return value in self._value
1321+
1322+
def index(self, suboid):
1323+
return self._value.index(suboid)
1324+
1325+
def isPrefixOf(self, other):
1326+
"""Indicate if this |ASN.1| object is a prefix of other |ASN.1| object.
1327+
Parameters
1328+
----------
1329+
other: |ASN.1| object
1330+
|ASN.1| object
1331+
Returns
1332+
-------
1333+
: :class:`bool`
1334+
:obj:`True` if this |ASN.1| object is a parent (e.g. prefix) of the other |ASN.1| object
1335+
or :obj:`False` otherwise.
1336+
"""
1337+
l = len(self)
1338+
if l <= len(other):
1339+
if self._value[:l] == other[:l]:
1340+
return True
1341+
return False
1342+
1343+
def prettyIn(self, value):
1344+
if isinstance(value, RelativeOID):
1345+
return tuple(value)
1346+
elif octets.isStringType(value):
1347+
if '-' in value:
1348+
raise error.PyAsn1Error(
1349+
'Malformed RELATIVE-OID %s at %s: %s' % (value, self.__class__.__name__, sys.exc_info()[1])
1350+
)
1351+
try:
1352+
return tuple([int(subOid) for subOid in value.split('.') if subOid])
1353+
except ValueError:
1354+
raise error.PyAsn1Error(
1355+
'Malformed RELATIVE-OID %s at %s: %s' % (value, self.__class__.__name__, sys.exc_info()[1])
1356+
)
1357+
1358+
try:
1359+
tupleOfInts = tuple([int(subOid) for subOid in value if subOid >= 0])
1360+
1361+
except (ValueError, TypeError):
1362+
raise error.PyAsn1Error(
1363+
'Malformed RELATIVE-OID %s at %s: %s' % (value, self.__class__.__name__, sys.exc_info()[1])
1364+
)
1365+
1366+
if len(tupleOfInts) == len(value):
1367+
return tupleOfInts
1368+
1369+
raise error.PyAsn1Error('Malformed RELATIVE-OID %s at %s' % (value, self.__class__.__name__))
1370+
1371+
def prettyOut(self, value):
1372+
return '.'.join([str(x) for x in value])
1373+
1374+
12471375
class Real(base.SimpleAsn1Type):
12481376
"""Create |ASN.1| schema or value object.
12491377

tests/codec/ber/test_decoder.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,76 @@ def testLarge2(self):
409409
) == ((2, 999, 18446744073709551535184467440737095), null)
410410

411411

412+
class RelativeOIDDecoderTestCase(BaseTestCase):
413+
def testThree(self):
414+
assert decoder.decode(
415+
ints2octs((13, 3, 5, 6, 7))
416+
) == ((5, 6, 7), null)
417+
418+
def testTwo(self):
419+
assert decoder.decode(
420+
ints2octs((13, 2, 5, 6))
421+
) == ((5, 6), null)
422+
423+
def testOne(self):
424+
obj, rest = decoder.decode(ints2octs((13, 1, 39)))
425+
assert str(obj) == '39'
426+
assert rest == null
427+
428+
def testNonLeading0x80(self):
429+
assert decoder.decode(
430+
ints2octs((13, 5, 85, 4, 129, 128, 0)),
431+
) == ((85, 4, 16384), null)
432+
433+
def testLeading0x80(self):
434+
try:
435+
decoder.decode(
436+
ints2octs((13, 5, 85, 4, 128, 129, 0))
437+
)
438+
except error.PyAsn1Error:
439+
pass
440+
else:
441+
assert 0, 'Leading 0x80 tolerated'
442+
443+
def testTagFormat(self):
444+
try:
445+
decoder.decode(ints2octs((38, 1, 239)))
446+
except error.PyAsn1Error:
447+
pass
448+
else:
449+
assert 0, 'wrong tagFormat worked out'
450+
451+
def testZeroLength(self):
452+
try:
453+
decoder.decode(ints2octs((13, 0, 0)))
454+
except error.PyAsn1Error:
455+
pass
456+
else:
457+
assert 0, 'zero length tolerated'
458+
459+
def testIndefiniteLength(self):
460+
try:
461+
decoder.decode(ints2octs((13, 128, 0)))
462+
except error.PyAsn1Error:
463+
pass
464+
else:
465+
assert 0, 'indefinite length tolerated'
466+
467+
def testReservedLength(self):
468+
try:
469+
decoder.decode(ints2octs((13, 255, 0)))
470+
except error.PyAsn1Error:
471+
pass
472+
else:
473+
assert 0, 'reserved length tolerated'
474+
475+
def testLarge(self):
476+
assert decoder.decode(
477+
ints2octs((0x0D, 0x13, 0x88, 0x37, 0x83, 0xC6, 0xDF, 0xD4, 0xCC, 0xB3, 0xFF, 0xFF, 0xFE, 0xF0, 0xB8, 0xD6, 0xB8, 0xCB, 0xE2, 0xB6, 0x47))
478+
) == ((1079, 18446744073709551535184467440737095), null)
479+
480+
481+
412482
class RealDecoderTestCase(BaseTestCase):
413483
def testChar(self):
414484
assert decoder.decode(

0 commit comments

Comments
 (0)