|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +"""A PCAP-NG block handed over as raw bytes parses and rebuilds. |
| 3 | +
|
| 4 | +This guards against GitHub issue #2354. ``block`` is typed to accept |
| 5 | +:obj:`bytes`, and the header schema writes such a block back verbatim past the |
| 6 | +Block Type -- but the schema the maker returned then held octets where the |
| 7 | +reader expected fields, so a Decryption Secrets Block built that way raised |
| 8 | +``AttributeError: 'bytes' object has no attribute 'length'``, and so did every |
| 9 | +other block type that got as far as the reader. |
| 10 | +
|
| 11 | +The octets are parsed off the buffer the construction path has just packed, the |
| 12 | +way the short-block path parses the body it kept: the block that comes back is |
| 13 | +the one the octets describe, and it rebuilds byte for byte. Octets that are no |
| 14 | +block at all are refused with the library's own errors rather than an |
| 15 | +:exc:`AttributeError`. |
| 16 | +
|
| 17 | +A Section Header Block handed raw octets with no section context is the one |
| 18 | +case left out, and it is not pinned here: it never reaches the reader, raising |
| 19 | +``AttributeError: 'PCAPNG' object has no attribute '_byte'`` out of the packer |
| 20 | +instead, since the byte order a section header carries is the one its own maker |
| 21 | +chose and no maker runs for raw octets. That is tracked separately. |
| 22 | +
|
| 23 | +Every case builds its own octets in memory and reads no capture. :mod:`pcapkit` |
| 24 | +is imported inside each test, after |
| 25 | +:func:`~tests._support.reimport_once_per_class`. |
| 26 | +
|
| 27 | +""" |
| 28 | + |
| 29 | +import unittest |
| 30 | +import warnings |
| 31 | +from typing import TYPE_CHECKING |
| 32 | + |
| 33 | +from tests._support import reimport_once_per_class |
| 34 | + |
| 35 | +if TYPE_CHECKING: |
| 36 | + from typing import Any |
| 37 | + |
| 38 | +#: Test tier (#1958): a hand-picked round trip, one case per block type. |
| 39 | +__tier__ = 'ALL' |
| 40 | + |
| 41 | +#: A TLS key log, four-octet aligned so the Decryption Secrets Block needs no |
| 42 | +#: padding. |
| 43 | +TLS_LOG = b'CLIENT_RANDOM 00 11\n' |
| 44 | +#: ``TLSK``, the Secrets Type of a TLS key log, as the field carries it. |
| 45 | +TLS_KEY_LOG = 0x544C534B |
| 46 | + |
| 47 | +#: Keywords that build one block for each of the types below, for the round trip |
| 48 | +#: that follows. A Section Header Block is not among them, for the reason the |
| 49 | +#: module docstring gives. A Packet Block and an Enhanced Packet Block both need |
| 50 | +#: a timestamp, their makers reading the parsed data model for a resolution |
| 51 | +#: without one. |
| 52 | +CASES = { |
| 53 | + 'IDB': ('Interface_Description_Block', {'linktype': 1, 'snaplen': 0xFFFF}), |
| 54 | + 'EPB': ('Enhanced_Packet_Block', {'packet_data': b'abcd', 'timestamp': 0}), |
| 55 | + 'PB': ('Packet_Block', {'packet_data': b'abcd', 'timestamp': 0}), |
| 56 | + 'SPB': ('Simple_Packet_Block', {'packet_data': b'abcde'}), |
| 57 | + 'ISB': ('Interface_Statistics_Block', {'interface_id': 0, 'timestamp': 0}), |
| 58 | + 'systemd': ('systemd_Journal_Export_Block', {'entries': b'MESSAGE=hi\n'}), |
| 59 | + 'CB': ('Custom_Block_that_rewriters_can_copy_into_new_files', |
| 60 | + {'pen': 1, 'data': b'abcd'}), |
| 61 | + 'unknown': ('Reserved_0x00000000', {'data': b'abcd'}), |
| 62 | +} |
| 63 | + |
| 64 | + |
| 65 | +def _dsb_body(secrets: 'bytes') -> 'bytes': |
| 66 | + """A little-endian Decryption Secrets Block past its Block Type.""" |
| 67 | + assert len(secrets) % 4 == 0, len(secrets) |
| 68 | + length = 20 + len(secrets) |
| 69 | + return b''.join(( |
| 70 | + length.to_bytes(4, 'little'), |
| 71 | + TLS_KEY_LOG.to_bytes(4, 'little'), |
| 72 | + len(secrets).to_bytes(4, 'little'), |
| 73 | + secrets, |
| 74 | + length.to_bytes(4, 'little'), |
| 75 | + )) |
| 76 | + |
| 77 | + |
| 78 | +class TestPCAPNGRawBlockBytes(unittest.TestCase): |
| 79 | + """Pin that ``make(block=<bytes>)`` builds a block rather than raising.""" |
| 80 | + |
| 81 | + def setUp(self) -> None: |
| 82 | + reimport_once_per_class(self) |
| 83 | + |
| 84 | + def _context(self) -> 'Any': |
| 85 | + """A one-section context with one Ethernet interface, as the engine builds it.""" |
| 86 | + from pcapkit.const.pcapng.block_type import BlockType |
| 87 | + from pcapkit.const.reg.linktype import LinkType |
| 88 | + from pcapkit.foundation.engines.pcapng import Context |
| 89 | + from pcapkit.protocols.misc.pcapng import PCAPNG |
| 90 | + |
| 91 | + section = PCAPNG(num=0, sct=1, ctx=None, type=BlockType.Section_Header_Block, block={}) |
| 92 | + context = Context(section.info) # type: Any |
| 93 | + interface = PCAPNG(num=1, sct=1, ctx=context, type=BlockType.Interface_Description_Block, |
| 94 | + block={'linktype': LinkType.ETHERNET, 'snaplen': 0x40000}) |
| 95 | + context.interfaces.append(interface.info) |
| 96 | + return context |
| 97 | + |
| 98 | + def test_a_dsb_handed_its_octets_parses_them(self) -> None: |
| 99 | + """#2354: the issue's own case, a DSB built from raw bytes.""" |
| 100 | + from pcapkit.const.pcapng.block_type import BlockType |
| 101 | + from pcapkit.const.pcapng.secrets_type import SecretsType |
| 102 | + from pcapkit.const.pcapng.tls_key_label import TLSKeyLabel |
| 103 | + from pcapkit.protocols.misc.pcapng import PCAPNG |
| 104 | + |
| 105 | + body = _dsb_body(TLS_LOG) |
| 106 | + block = PCAPNG(num=2, sct=1, ctx=self._context(), |
| 107 | + type=BlockType.Decryption_Secrets_Block, block=body) |
| 108 | + |
| 109 | + self.assertEqual(block.info.secrets_type, SecretsType.TLS_Key_Log) |
| 110 | + self.assertEqual(block.info.secrets_length, len(TLS_LOG)) |
| 111 | + self.assertEqual(block.info.secrets_data.data, TLS_LOG.decode()) |
| 112 | + self.assertIn(TLSKeyLabel.CLIENT_RANDOM, block.info.secrets_data.entries) |
| 113 | + # The octets are written back exactly as handed over, after the type. |
| 114 | + self.assertEqual(block.data, |
| 115 | + int(BlockType.Decryption_Secrets_Block).to_bytes(4, 'little') + body) |
| 116 | + |
| 117 | + def test_every_block_type_handed_its_octets_round_trips(self) -> None: |
| 118 | + """#2354: raw octets parse as the block they describe, and rebuild byte for byte.""" |
| 119 | + from pcapkit.const.pcapng.block_type import BlockType |
| 120 | + from pcapkit.protocols.misc.pcapng import PCAPNG |
| 121 | + |
| 122 | + for label, (name, keywords) in CASES.items(): |
| 123 | + with self.subTest(block=label): |
| 124 | + type_ = getattr(BlockType, name) |
| 125 | + context = self._context() |
| 126 | + with warnings.catch_warnings(): |
| 127 | + # a Packet Block warns that the block is obsolete |
| 128 | + warnings.simplefilter('ignore') |
| 129 | + built = PCAPNG(num=2, sct=1, ctx=context, type=type_, block=keywords) |
| 130 | + again = PCAPNG(num=2, sct=1, ctx=context, type=type_, block=built.data[4:]) |
| 131 | + |
| 132 | + self.assertEqual(again.data, built.data) |
| 133 | + self.assertEqual(str(again.info), str(built.info)) |
| 134 | + |
| 135 | + def test_octets_that_are_no_block_are_refused(self) -> None: |
| 136 | + """#2354: a short or impossible block raises the library's own error.""" |
| 137 | + from pcapkit.const.pcapng.block_type import BlockType |
| 138 | + from pcapkit.protocols.misc.pcapng import PCAPNG |
| 139 | + from pcapkit.utilities.exceptions import ProtocolError, StreamEOFError |
| 140 | + |
| 141 | + context = self._context() |
| 142 | + # Shorter than the twelve octets a block takes, the Block Type counted. |
| 143 | + for body in (b'', b'raw', b'\x00' * 7): |
| 144 | + with self.subTest(body=body): |
| 145 | + with self.assertRaisesRegex(StreamEOFError, r'fewer than the 12 a block needs'): |
| 146 | + PCAPNG(num=2, sct=1, ctx=context, |
| 147 | + type=BlockType.Decryption_Secrets_Block, block=body) |
| 148 | + |
| 149 | + # Long enough, but declaring a Block Total Length of zero. |
| 150 | + for body in (b'\x00' * 8, b'\x00' * 12): |
| 151 | + with self.subTest(body=body): |
| 152 | + with warnings.catch_warnings(): |
| 153 | + warnings.simplefilter('ignore') |
| 154 | + with self.assertRaisesRegex(ProtocolError, r'invalid length: 0'): |
| 155 | + PCAPNG(num=2, sct=1, ctx=context, |
| 156 | + type=BlockType.Decryption_Secrets_Block, block=body) |
| 157 | + |
| 158 | + |
| 159 | +if __name__ == '__main__': |
| 160 | + unittest.main() |
0 commit comments