Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pyasn1 for openSUSE:Factory checked in at 2026-03-19 17:26:44 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pyasn1 (Old) and /work/SRC/openSUSE:Factory/.python-pyasn1.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pyasn1" Thu Mar 19 17:26:44 2026 rev:48 rq:1340912 version:0.6.3 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pyasn1/python-pyasn1.changes 2026-01-21 14:14:29.171661058 +0100 +++ /work/SRC/openSUSE:Factory/.python-pyasn1.new.8177/python-pyasn1.changes 2026-03-19 17:26:45.731472908 +0100 @@ -1,0 +2,10 @@ +Wed Mar 18 13:44:02 UTC 2026 - Daniel Garcia <[email protected]> + +- Update to 0.6.3 (bsc#1259803, CVE-2026-30922) + * Added nesting depth limit to ASN.1 decoder to prevent stack + overflow from deeply nested structures (CVE-2026-30922). + * Fixed OverflowError from oversized BER length field. + * Fixed DeprecationWarning stacklevel for deprecated attributes. + * Fixed asDateTime incorrect fractional seconds parsing. + +------------------------------------------------------------------- Old: ---- pyasn1-0.6.2.tar.gz New: ---- pyasn1-0.6.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pyasn1.spec ++++++ --- /var/tmp/diff_new_pack.QsL1YD/_old 2026-03-19 17:26:46.443502441 +0100 +++ /var/tmp/diff_new_pack.QsL1YD/_new 2026-03-19 17:26:46.447502607 +0100 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-pyasn1 -Version: 0.6.2 +Version: 0.6.3 Release: 0 Summary: ASN.1 types and codecs License: BSD-2-Clause ++++++ pyasn1-0.6.2.tar.gz -> pyasn1-0.6.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/CHANGES.rst new/pyasn1-0.6.3/CHANGES.rst --- old/pyasn1-0.6.2/CHANGES.rst 2026-01-16 18:54:37.000000000 +0100 +++ new/pyasn1-0.6.3/CHANGES.rst 2026-03-17 02:06:33.000000000 +0100 @@ -1,3 +1,19 @@ +Revision 0.6.3, released 16-03-2026 +--------------------------------------- + +- CVE-2026-30922 (GHSA-jr27-m4p2-rc6r): Added nesting depth + limit to ASN.1 decoder to prevent stack overflow from deeply + nested structures (thanks for reporting, romanticpragmatism) +- Fixed OverflowError from oversized BER length field + [issue #54](https://github.com/pyasn1/pyasn1/issues/54) + [pr #100](https://github.com/pyasn1/pyasn1/pull/100) +- Fixed DeprecationWarning stacklevel for deprecated attributes + [issue #86](https://github.com/pyasn1/pyasn1/issues/86) + [pr #101](https://github.com/pyasn1/pyasn1/pull/101) +- Fixed asDateTime incorrect fractional seconds parsing + [issue #81](https://github.com/pyasn1/pyasn1/issues/81) + [pr #102](https://github.com/pyasn1/pyasn1/pull/102) + Revision 0.6.2, released 16-01-2026 --------------------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/PKG-INFO new/pyasn1-0.6.3/PKG-INFO --- old/pyasn1-0.6.2/PKG-INFO 2026-01-16 19:03:37.415287000 +0100 +++ new/pyasn1-0.6.3/PKG-INFO 2026-03-17 02:06:42.622166200 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: pyasn1 -Version: 0.6.2 +Version: 0.6.3 Summary: Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208) Author-email: Ilya Etingof <[email protected]> Maintainer: pyasn1 maintenance organization diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1/__init__.py new/pyasn1-0.6.3/pyasn1/__init__.py --- old/pyasn1-0.6.2/pyasn1/__init__.py 2026-01-16 18:54:37.000000000 +0100 +++ new/pyasn1-0.6.3/pyasn1/__init__.py 2026-03-17 02:06:33.000000000 +0100 @@ -1,2 +1,2 @@ # https://www.python.org/dev/peps/pep-0396/ -__version__ = '0.6.2' +__version__ = '0.6.3' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1/codec/ber/decoder.py new/pyasn1-0.6.3/pyasn1/codec/ber/decoder.py --- old/pyasn1-0.6.2/pyasn1/codec/ber/decoder.py 2026-01-16 18:54:20.000000000 +0100 +++ new/pyasn1-0.6.3/pyasn1/codec/ber/decoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -36,6 +36,10 @@ # Maximum number of continuation octets (high-bit set) allowed per OID arc. # 20 octets allows up to 140-bit integers, supporting UUID-based OIDs MAX_OID_ARC_CONTINUATION_OCTETS = 20 +MAX_NESTING_DEPTH = 100 + +# Maximum number of bytes in a BER length field (8 bytes = up to 2^64-1) +MAX_LENGTH_OCTETS = 8 class AbstractPayloadDecoder(object): @@ -1565,6 +1569,15 @@ decodeFun=None, substrateFun=None, **options): + _nestingLevel = options.get('_nestingLevel', 0) + + if _nestingLevel > MAX_NESTING_DEPTH: + raise error.PyAsn1Error( + 'ASN.1 structure nesting depth exceeds limit (%d)' % MAX_NESTING_DEPTH + ) + + options['_nestingLevel'] = _nestingLevel + 1 + allowEoo = options.pop('allowEoo', False) if LOG: @@ -1682,13 +1695,18 @@ elif firstOctet > 128: size = firstOctet & 0x7F + + if size > MAX_LENGTH_OCTETS: + raise error.PyAsn1Error( + 'BER length field size %d exceeds limit (%d)' % ( + size, MAX_LENGTH_OCTETS) + ) + # encoded in size bytes for encodedLength in readFromStream(substrate, size, options): if isinstance(encodedLength, SubstrateUnderrunError): yield encodedLength encodedLength = list(encodedLength) - # missing check on maximum size, which shouldn't be a - # problem, we can handle more than is possible if len(encodedLength) != size: raise error.SubstrateUnderrunError( '%s<%s at %s' % (size, len(encodedLength), tagSet) @@ -2202,6 +2220,6 @@ def __getattr__(attr: str): if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): - warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) return globals()[newAttr] raise AttributeError(attr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1/codec/ber/encoder.py new/pyasn1-0.6.3/pyasn1/codec/ber/encoder.py --- old/pyasn1-0.6.2/pyasn1/codec/ber/encoder.py 2025-10-03 00:47:58.000000000 +0200 +++ new/pyasn1-0.6.3/pyasn1/codec/ber/encoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -949,6 +949,6 @@ def __getattr__(attr: str): if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): - warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) return globals()[newAttr] raise AttributeError(attr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1/codec/cer/decoder.py new/pyasn1-0.6.3/pyasn1/codec/cer/decoder.py --- old/pyasn1-0.6.2/pyasn1/codec/cer/decoder.py 2025-10-03 00:47:58.000000000 +0200 +++ new/pyasn1-0.6.3/pyasn1/codec/cer/decoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -144,6 +144,6 @@ def __getattr__(attr: str): if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): - warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) return globals()[newAttr] raise AttributeError(attr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1/codec/cer/encoder.py new/pyasn1-0.6.3/pyasn1/codec/cer/encoder.py --- old/pyasn1-0.6.2/pyasn1/codec/cer/encoder.py 2025-10-03 00:47:58.000000000 +0200 +++ new/pyasn1-0.6.3/pyasn1/codec/cer/encoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -326,6 +326,6 @@ def __getattr__(attr: str): if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): - warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) return globals()[newAttr] raise AttributeError(attr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1/codec/der/decoder.py new/pyasn1-0.6.3/pyasn1/codec/der/decoder.py --- old/pyasn1-0.6.2/pyasn1/codec/der/decoder.py 2025-10-03 00:47:58.000000000 +0200 +++ new/pyasn1-0.6.3/pyasn1/codec/der/decoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -115,6 +115,6 @@ def __getattr__(attr: str): if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): - warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) return globals()[newAttr] raise AttributeError(attr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1/codec/der/encoder.py new/pyasn1-0.6.3/pyasn1/codec/der/encoder.py --- old/pyasn1-0.6.2/pyasn1/codec/der/encoder.py 2025-10-03 00:47:58.000000000 +0200 +++ new/pyasn1-0.6.3/pyasn1/codec/der/encoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -121,6 +121,6 @@ def __getattr__(attr: str): if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): - warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) return globals()[newAttr] raise AttributeError(attr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1/codec/native/decoder.py new/pyasn1-0.6.3/pyasn1/codec/native/decoder.py --- old/pyasn1-0.6.2/pyasn1/codec/native/decoder.py 2025-10-03 00:47:58.000000000 +0200 +++ new/pyasn1-0.6.3/pyasn1/codec/native/decoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -239,6 +239,6 @@ def __getattr__(attr: str): if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): - warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) return globals()[newAttr] raise AttributeError(attr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1/codec/native/encoder.py new/pyasn1-0.6.3/pyasn1/codec/native/encoder.py --- old/pyasn1-0.6.2/pyasn1/codec/native/encoder.py 2025-10-03 00:47:58.000000000 +0200 +++ new/pyasn1-0.6.3/pyasn1/codec/native/encoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -280,6 +280,6 @@ def __getattr__(attr: str): if newAttr := {"tagMap": "TAG_MAP", "typeMap": "TYPE_MAP"}.get(attr): - warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning, stacklevel=2) return globals()[newAttr] raise AttributeError(attr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1/type/useful.py new/pyasn1-0.6.3/pyasn1/type/useful.py --- old/pyasn1-0.6.2/pyasn1/type/useful.py 2025-10-03 00:48:00.000000000 +0200 +++ new/pyasn1-0.6.3/pyasn1/type/useful.py 2026-03-17 02:06:33.000000000 +0100 @@ -102,7 +102,8 @@ text, _, ms = text.partition(',') try: - ms = int(ms) * 1000 + # Normalize variable-length fraction to microseconds + ms = int(ms.ljust(6, '0')[:6]) except ValueError: raise error.PyAsn1Error('bad sub-second time specification %s' % self) @@ -139,8 +140,8 @@ new instance of |ASN.1| value """ text = dt.strftime(cls._yearsDigits == 4 and '%Y%m%d%H%M%S' or '%y%m%d%H%M%S') - if cls._hasSubsecond: - text += '.%d' % (dt.microsecond // 1000) + if cls._hasSubsecond and dt.microsecond: + text += ('.%06d' % dt.microsecond).rstrip('0') if dt.utcoffset(): seconds = dt.utcoffset().seconds diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/pyasn1.egg-info/PKG-INFO new/pyasn1-0.6.3/pyasn1.egg-info/PKG-INFO --- old/pyasn1-0.6.2/pyasn1.egg-info/PKG-INFO 2026-01-16 19:03:37.000000000 +0100 +++ new/pyasn1-0.6.3/pyasn1.egg-info/PKG-INFO 2026-03-17 02:06:42.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: pyasn1 -Version: 0.6.2 +Version: 0.6.3 Summary: Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208) Author-email: Ilya Etingof <[email protected]> Maintainer: pyasn1 maintenance organization diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/tests/codec/ber/test_decoder.py new/pyasn1-0.6.3/tests/codec/ber/test_decoder.py --- old/pyasn1-0.6.2/tests/codec/ber/test_decoder.py 2026-01-16 18:54:20.000000000 +0100 +++ new/pyasn1-0.6.3/tests/codec/ber/test_decoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -2117,6 +2117,177 @@ os.remove(path) +class LengthFieldLimitTestCase(BaseTestCase): + """Test protection against oversized BER length fields.""" + + def testOversizedLengthField(self): + """Length field exceeding MAX_LENGTH_OCTETS must raise PyAsn1Error.""" + # 0x89 = long form, 9-byte length (exceeds 8-byte limit) + payload = b'\x02\x89' + b'\xFF' * 9 + b'\x01' + try: + decoder.decode(payload) + except error.PyAsn1Error: + pass + else: + assert False, 'Oversized length field not rejected' + + def testNoOverflowError(self): + """Must raise PyAsn1Error, not OverflowError.""" + payload = b'\x02\x90' + b'\xFF' * 16 + b'\x01' + try: + decoder.decode(payload) + except error.PyAsn1Error: + pass + except OverflowError: + assert False, 'Got OverflowError instead of PyAsn1Error' + + def testMaxAllowedLengthFieldWorks(self): + """8-byte length field (the maximum allowed) must be accepted.""" + # 0x88 = long form, 8-byte length, value = 5 + payload = b'\x04\x88' + b'\x00' * 7 + b'\x05' + b'hello' + asn1Object, _ = decoder.decode(payload) + assert bytes(asn1Object) == b'hello', \ + 'Failed to decode with 8-byte length field' + + def testNormalLengthFieldWorks(self): + """Standard short-form and long-form lengths must still work.""" + # Short form: length < 128 + asn1Object, _ = decoder.decode(b'\x02\x01\x05') + assert asn1Object == 5 + + # Long form 1-byte: 0x81 + 1 byte length + payload = b'\x04\x81\x05' + b'hello' + asn1Object, _ = decoder.decode(payload) + assert bytes(asn1Object) == b'hello' + + def testErrorMessageContainsLimit(self): + """Error message must indicate the length field size limit.""" + payload = b'\x02\x89' + b'\xFF' * 9 + b'\x01' + try: + decoder.decode(payload) + except error.PyAsn1Error as exc: + assert 'length field size' in str(exc).lower(), \ + 'Error message missing size info: %s' % exc + else: + assert False, 'Expected PyAsn1Error' + + +class NestingDepthLimitTestCase(BaseTestCase): + """Test protection against deeply nested ASN.1 structures (CVE prevention).""" + + def testIndefLenSequenceNesting(self): + """Deeply nested indefinite-length SEQUENCEs must raise PyAsn1Error.""" + # Each \x30\x80 opens a new indefinite-length SEQUENCE + payload = b'\x30\x80' * 200 + try: + decoder.decode(payload) + except error.PyAsn1Error: + pass + else: + assert False, 'Deeply nested indef-length SEQUENCEs not rejected' + + def testIndefLenSetNesting(self): + """Deeply nested indefinite-length SETs must raise PyAsn1Error.""" + # Each \x31\x80 opens a new indefinite-length SET + payload = b'\x31\x80' * 200 + try: + decoder.decode(payload) + except error.PyAsn1Error: + pass + else: + assert False, 'Deeply nested indef-length SETs not rejected' + + def testDefiniteLenNesting(self): + """Deeply nested definite-length SEQUENCEs must raise PyAsn1Error.""" + inner = b'\x05\x00' # NULL + for _ in range(200): + length = len(inner) + if length < 128: + inner = b'\x30' + bytes([length]) + inner + else: + length_bytes = length.to_bytes( + (length.bit_length() + 7) // 8, 'big') + inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \ + length_bytes + inner + try: + decoder.decode(inner) + except error.PyAsn1Error: + pass + else: + assert False, 'Deeply nested definite-length SEQUENCEs not rejected' + + def testNestingUnderLimitWorks(self): + """Nesting within the limit must decode successfully.""" + inner = b'\x05\x00' # NULL + for _ in range(50): + length = len(inner) + if length < 128: + inner = b'\x30' + bytes([length]) + inner + else: + length_bytes = length.to_bytes( + (length.bit_length() + 7) // 8, 'big') + inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \ + length_bytes + inner + asn1Object, _ = decoder.decode(inner) + assert asn1Object is not None, 'Valid nested structure rejected' + + def testSiblingsDontIncreaseDepth(self): + """Sibling elements at the same level must not inflate depth count.""" + # SEQUENCE containing 200 INTEGER siblings - should decode fine + components = b'\x02\x01\x01' * 200 # 200 x INTEGER(1) + length = len(components) + length_bytes = length.to_bytes( + (length.bit_length() + 7) // 8, 'big') + payload = b'\x30' + bytes([0x80 | len(length_bytes)]) + \ + length_bytes + components + asn1Object, _ = decoder.decode(payload) + assert asn1Object is not None, 'Siblings incorrectly rejected' + + def testErrorMessageContainsLimit(self): + """Error message must indicate the nesting depth limit.""" + payload = b'\x30\x80' * 200 + try: + decoder.decode(payload) + except error.PyAsn1Error as exc: + assert 'nesting depth' in str(exc).lower(), \ + 'Error message missing depth info: %s' % exc + else: + assert False, 'Expected PyAsn1Error' + + def testNoRecursionError(self): + """Must raise PyAsn1Error, not RecursionError.""" + payload = b'\x30\x80' * 50000 + try: + decoder.decode(payload) + except error.PyAsn1Error: + pass + except RecursionError: + assert False, 'Got RecursionError instead of PyAsn1Error' + + def testMixedNesting(self): + """Mixed SEQUENCE and SET nesting must be caught.""" + # Alternate SEQUENCE (0x30) and SET (0x31) with indef length + payload = b'' + for i in range(200): + payload += b'\x30\x80' if i % 2 == 0 else b'\x31\x80' + try: + decoder.decode(payload) + except error.PyAsn1Error: + pass + else: + assert False, 'Mixed nesting not rejected' + + def testWithSchema(self): + """Deeply nested structures must be caught even with schema.""" + payload = b'\x30\x80' * 200 + try: + decoder.decode(payload, asn1Spec=univ.Sequence()) + except error.PyAsn1Error: + pass + else: + assert False, 'Deeply nested with schema not rejected' + + class NonStreamingCompatibilityTestCase(BaseTestCase): def setUp(self): from pyasn1 import debug diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/tests/codec/cer/test_decoder.py new/pyasn1-0.6.3/tests/codec/cer/test_decoder.py --- old/pyasn1-0.6.2/tests/codec/cer/test_decoder.py 2025-10-03 00:48:01.000000000 +0200 +++ new/pyasn1-0.6.3/tests/codec/cer/test_decoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -363,6 +363,30 @@ assert s[1][0] == univ.OctetString(hexValue='02010C') +class NestingDepthLimitTestCase(BaseTestCase): + """Test CER decoder protection against deeply nested structures.""" + + def testIndefLenNesting(self): + """Deeply nested indefinite-length SEQUENCEs must raise PyAsn1Error.""" + payload = b'\x30\x80' * 200 + try: + decoder.decode(payload) + except PyAsn1Error: + pass + else: + assert False, 'Deeply nested indef-length SEQUENCEs not rejected' + + def testNoRecursionError(self): + """Must raise PyAsn1Error, not RecursionError.""" + payload = b'\x30\x80' * 50000 + try: + decoder.decode(payload) + except PyAsn1Error: + pass + except RecursionError: + assert False, 'Got RecursionError instead of PyAsn1Error' + + suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) if __name__ == '__main__': diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/tests/codec/der/test_decoder.py new/pyasn1-0.6.3/tests/codec/der/test_decoder.py --- old/pyasn1-0.6.2/tests/codec/der/test_decoder.py 2025-10-03 00:48:01.000000000 +0200 +++ new/pyasn1-0.6.3/tests/codec/der/test_decoder.py 2026-03-17 02:06:33.000000000 +0100 @@ -361,6 +361,48 @@ assert s[1][0] == univ.OctetString(hexValue='02010C') +class NestingDepthLimitTestCase(BaseTestCase): + """Test DER decoder protection against deeply nested structures.""" + + def testDefiniteLenNesting(self): + """Deeply nested definite-length SEQUENCEs must raise PyAsn1Error.""" + inner = b'\x05\x00' # NULL + for _ in range(200): + length = len(inner) + if length < 128: + inner = b'\x30' + bytes([length]) + inner + else: + length_bytes = length.to_bytes( + (length.bit_length() + 7) // 8, 'big') + inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \ + length_bytes + inner + try: + decoder.decode(inner) + except PyAsn1Error: + pass + else: + assert False, 'Deeply nested definite-length SEQUENCEs not rejected' + + def testNoRecursionError(self): + """Must raise PyAsn1Error, not RecursionError.""" + inner = b'\x05\x00' + for _ in range(200): + length = len(inner) + if length < 128: + inner = b'\x30' + bytes([length]) + inner + else: + length_bytes = length.to_bytes( + (length.bit_length() + 7) // 8, 'big') + inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \ + length_bytes + inner + try: + decoder.decode(inner) + except PyAsn1Error: + pass + except RecursionError: + assert False, 'Got RecursionError instead of PyAsn1Error' + + suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) if __name__ == '__main__': diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyasn1-0.6.2/tests/type/test_useful.py new/pyasn1-0.6.3/tests/type/test_useful.py --- old/pyasn1-0.6.2/tests/type/test_useful.py 2025-10-03 00:48:03.000000000 +0200 +++ new/pyasn1-0.6.3/tests/type/test_useful.py 2026-03-17 02:06:33.000000000 +0100 @@ -41,7 +41,13 @@ class GeneralizedTimeTestCase(BaseTestCase): def testFromDateTime(self): - assert useful.GeneralizedTime.fromDateTime(datetime.datetime(2017, 7, 11, 0, 1, 2, 3000, tzinfo=UTC)) == '20170711000102.3Z' + assert useful.GeneralizedTime.fromDateTime(datetime.datetime(2017, 7, 11, 0, 1, 2, 3000, tzinfo=UTC)) == '20170711000102.003Z' + + def testFromDateTimeNoMicrosecond(self): + assert useful.GeneralizedTime.fromDateTime(datetime.datetime(2017, 7, 11, 0, 1, 2, 0, tzinfo=UTC)) == '20170711000102Z' + + def testFromDateTimeFullMicrosecond(self): + assert useful.GeneralizedTime.fromDateTime(datetime.datetime(2017, 7, 11, 0, 1, 2, 123456, tzinfo=UTC)) == '20170711000102.123456Z' def testToDateTime0(self): assert datetime.datetime(2017, 7, 11, 0, 1, 2) == useful.GeneralizedTime('20170711000102').asDateTime @@ -50,19 +56,19 @@ assert datetime.datetime(2017, 7, 11, 0, 1, 2, tzinfo=UTC) == useful.GeneralizedTime('20170711000102Z').asDateTime def testToDateTime2(self): - assert datetime.datetime(2017, 7, 11, 0, 1, 2, 3000, tzinfo=UTC) == useful.GeneralizedTime('20170711000102.3Z').asDateTime + assert datetime.datetime(2017, 7, 11, 0, 1, 2, 300000, tzinfo=UTC) == useful.GeneralizedTime('20170711000102.3Z').asDateTime def testToDateTime3(self): - assert datetime.datetime(2017, 7, 11, 0, 1, 2, 3000, tzinfo=UTC) == useful.GeneralizedTime('20170711000102,3Z').asDateTime + assert datetime.datetime(2017, 7, 11, 0, 1, 2, 300000, tzinfo=UTC) == useful.GeneralizedTime('20170711000102,3Z').asDateTime def testToDateTime4(self): - assert datetime.datetime(2017, 7, 11, 0, 1, 2, 3000, tzinfo=UTC) == useful.GeneralizedTime('20170711000102.3+0000').asDateTime + assert datetime.datetime(2017, 7, 11, 0, 1, 2, 300000, tzinfo=UTC) == useful.GeneralizedTime('20170711000102.3+0000').asDateTime def testToDateTime5(self): - assert datetime.datetime(2017, 7, 11, 0, 1, 2, 3000, tzinfo=UTC2) == useful.GeneralizedTime('20170711000102.3+0200').asDateTime + assert datetime.datetime(2017, 7, 11, 0, 1, 2, 300000, tzinfo=UTC2) == useful.GeneralizedTime('20170711000102.3+0200').asDateTime def testToDateTime6(self): - assert datetime.datetime(2017, 7, 11, 0, 1, 2, 3000, tzinfo=UTC2) == useful.GeneralizedTime('20170711000102.3+02').asDateTime + assert datetime.datetime(2017, 7, 11, 0, 1, 2, 300000, tzinfo=UTC2) == useful.GeneralizedTime('20170711000102.3+02').asDateTime def testToDateTime7(self): assert datetime.datetime(2017, 7, 11, 0, 1) == useful.GeneralizedTime('201707110001').asDateTime @@ -70,6 +76,28 @@ def testToDateTime8(self): assert datetime.datetime(2017, 7, 11, 0) == useful.GeneralizedTime('2017071100').asDateTime + def testToDateTimeTwoDigitFraction(self): + """Two-digit fraction .18 means 0.18 seconds = 180000 microseconds (issue #81).""" + assert datetime.datetime(2016, 8, 6, 11, 59, 52, 180000, tzinfo=UTC) == useful.GeneralizedTime('20160806115952.18Z').asDateTime + + def testToDateTimeThreeDigitFraction(self): + """Three-digit fraction .123 means 0.123 seconds = 123000 microseconds.""" + assert datetime.datetime(2017, 7, 11, 0, 1, 2, 123000, tzinfo=UTC) == useful.GeneralizedTime('20170711000102.123Z').asDateTime + + def testToDateTimeSixDigitFraction(self): + """Six-digit fraction .123456 means 123456 microseconds.""" + assert datetime.datetime(2017, 7, 11, 0, 1, 2, 123456, tzinfo=UTC) == useful.GeneralizedTime('20170711000102.123456Z').asDateTime + + def testToDateTimeSevenDigitFractionTruncated(self): + """Seven-digit fraction .1234567 is truncated to 123456 microseconds.""" + assert datetime.datetime(2017, 7, 11, 0, 1, 2, 123456, tzinfo=UTC) == useful.GeneralizedTime('20170711000102.1234567Z').asDateTime + + def testFromDateTimeRoundTrip(self): + """fromDateTime and asDateTime should round-trip correctly.""" + original = datetime.datetime(2017, 7, 11, 0, 1, 2, 180000, tzinfo=UTC) + encoded = useful.GeneralizedTime.fromDateTime(original) + assert original == encoded.asDateTime + def testCopy(self): dt = useful.GeneralizedTime("20170916234254+0130").asDateTime assert dt == deepcopy(dt)
