From a863a166be04b1e313b59e7c271f87f86e109358 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:31:34 +0000 Subject: [PATCH 1/6] Implement CBOR parser following ASN.1 paradigm Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> Address code review comments: improve error messages and implement proper half-float decoding Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> Add cbor2 interoperability tests (cbor2 used ONLY in tests) Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> Document cbor2 as test-only dependency in test file header Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> Add CBOR documentation to advanced_usage.rst following ASN.1 pattern Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> Add cbor2 to tox.ini testenv deps for CBOR interoperability tests Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- doc/scapy/advanced_usage.rst | 1444 ++++++++++++++++++++++++++++++++++ scapy/cbor/__init__.py | 81 ++ scapy/cbor/cbor.py | 357 +++++++++ scapy/cbor/cborcodec.py | 702 +++++++++++++++++ test/scapy/layers/cbor.uts | 557 +++++++++++++ tox.ini | 1 + 6 files changed, 3142 insertions(+) create mode 100644 doc/scapy/advanced_usage.rst create mode 100644 scapy/cbor/__init__.py create mode 100644 scapy/cbor/cbor.py create mode 100644 scapy/cbor/cborcodec.py create mode 100644 test/scapy/layers/cbor.uts diff --git a/doc/scapy/advanced_usage.rst b/doc/scapy/advanced_usage.rst new file mode 100644 index 00000000000..6606cb5cdc2 --- /dev/null +++ b/doc/scapy/advanced_usage.rst @@ -0,0 +1,1444 @@ +************** +Advanced usage +************** + +ASN.1 and SNMP +============== + +What is ASN.1? +-------------- + +.. note:: + + This is only my view on ASN.1, explained as simply as possible. For more theoretical or academic views, I'm sure you'll find better on the Internet. + +ASN.1 is a notation whose goal is to specify formats for data exchange. It is independent of the way data is encoded. Data encoding is specified in Encoding Rules. + +The most used encoding rules are BER (Basic Encoding Rules) and DER (Distinguished Encoding Rules). Both look the same, but the latter is specified to guarantee uniqueness of encoding. This property is quite interesting when speaking about cryptography, hashes, and signatures. + +ASN.1 provides basic objects: integers, many kinds of strings, floats, booleans, containers, etc. They are grouped in the so-called Universal class. A given protocol can provide other objects which will be grouped in the Context class. For example, SNMP defines PDU_GET or PDU_SET objects. There are also the Application and Private classes. + +Each of these objects is given a tag that will be used by the encoding rules. Tags from 1 are used for Universal class. 1 is boolean, 2 is an integer, 3 is a bit string, 6 is an OID, 48 is for a sequence. Tags from the ``Context`` class begin at 0xa0. When encountering an object tagged by 0xa0, we'll need to know the context to be able to decode it. For example, in SNMP context, 0xa0 is a PDU_GET object, while in X509 context, it is a container for the certificate version. + +Other objects are created by assembling all those basic brick objects. The composition is done using sequences and arrays (sets) of previously defined or existing objects. The final object (an X509 certificate, a SNMP packet) is a tree whose non-leaf nodes are sequences and sets objects (or derived context objects), and whose leaf nodes are integers, strings, OID, etc. + +Scapy and ASN.1 +--------------- + +Scapy provides a way to easily encode or decode ASN.1 and also program those encoders/decoders. It is quite laxer than what an ASN.1 parser should be, and it kind of ignores constraints. It won't replace neither an ASN.1 parser nor an ASN.1 compiler. Actually, it has been written to be able to encode and decode broken ASN.1. It can handle corrupted encoded strings and can also create those. + +ASN.1 engine +^^^^^^^^^^^^ + +Note: many of the classes definitions presented here use metaclasses. If you don't look precisely at the source code and you only rely on my captures, you may think they sometimes exhibit a kind of magic behavior. +`` +Scapy ASN.1 engine provides classes to link objects and their tags. They inherit from the ``ASN1_Class``. The first one is ``ASN1_Class_UNIVERSAL``, which provide tags for most Universal objects. Each new context (``SNMP``, ``X509``) will inherit from it and add its own objects. + +:: + + class ASN1_Class_UNIVERSAL(ASN1_Class): + name = "UNIVERSAL" + # [...] + BOOLEAN = 1 + INTEGER = 2 + BIT_STRING = 3 + # [...] + + class ASN1_Class_SNMP(ASN1_Class_UNIVERSAL): + name="SNMP" + PDU_GET = 0xa0 + PDU_NEXT = 0xa1 + PDU_RESPONSE = 0xa2 + + class ASN1_Class_X509(ASN1_Class_UNIVERSAL): + name="X509" + CONT0 = 0xa0 + CONT1 = 0xa1 + # [...] + +All ASN.1 objects are represented by simple Python instances that act as nutshells for the raw values. The simple logic is handled by ``ASN1_Object`` whose they inherit from. Hence they are quite simple:: + + class ASN1_INTEGER(ASN1_Object): + tag = ASN1_Class_UNIVERSAL.INTEGER + + class ASN1_STRING(ASN1_Object): + tag = ASN1_Class_UNIVERSAL.STRING + + class ASN1_BIT_STRING(ASN1_STRING): + tag = ASN1_Class_UNIVERSAL.BIT_STRING + +These instances can be assembled to create an ASN.1 tree:: + + >>> x=ASN1_SEQUENCE([ASN1_INTEGER(7),ASN1_STRING("egg"),ASN1_SEQUENCE([ASN1_BOOLEAN(False)])]) + >>> x + , , ]]>]]> + >>> x.show() + # ASN1_SEQUENCE: + + + # ASN1_SEQUENCE: + + +Encoding engines +^^^^^^^^^^^^^^^^^ + +As with the standard, ASN.1 and encoding are independent. We have just seen how to create a compounded ASN.1 object. To encode or decode it, we need to choose an encoding rule. Scapy provides only BER for the moment (actually, it may be DER. DER looks like BER except only minimal encoding is authorised which may well be what I did). I call this an ASN.1 codec. + +Encoding and decoding are done using class methods provided by the codec. For example the ``BERcodec_INTEGER`` class provides a ``.enc()`` and a ``.dec()`` class methods that can convert between an encoded string and a value of their type. They all inherit from BERcodec_Object which is able to decode objects from any type:: + + >>> BERcodec_INTEGER.enc(7) + '\x02\x01\x07' + >>> BERcodec_BIT_STRING.enc("egg") + '\x03\x03egg' + >>> BERcodec_STRING.enc("egg") + '\x04\x03egg' + >>> BERcodec_STRING.dec('\x04\x03egg') + (, '') + >>> BERcodec_STRING.dec('\x03\x03egg') + Traceback (most recent call last): + File "", line 1, in ? + File "/usr/bin/scapy", line 2099, in dec + return cls.do_dec(s, context, safe) + File "/usr/bin/scapy", line 2178, in do_dec + l,s,t = cls.check_type_check_len(s) + File "/usr/bin/scapy", line 2076, in check_type_check_len + l,s3 = cls.check_type_get_len(s) + File "/usr/bin/scapy", line 2069, in check_type_get_len + s2 = cls.check_type(s) + File "/usr/bin/scapy", line 2065, in check_type + (cls.__name__, ord(s[0]), ord(s[0]),cls.tag), remaining=s) + BER_BadTag_Decoding_Error: BERcodec_STRING: Got tag [3/0x3] while expecting + ### Already decoded ### + None + ### Remaining ### + '\x03\x03egg' + >>> BERcodec_Object.dec('\x03\x03egg') + (, '') + +ASN.1 objects are encoded using their ``.enc()`` method. This method must be called with the codec we want to use. All codecs are referenced in the ASN1_Codecs object. ``raw()`` can also be used. In this case, the default codec (``conf.ASN1_default_codec``) will be used. + +:: + + >>> x.enc(ASN1_Codecs.BER) + '0\r\x02\x01\x07\x04\x03egg0\x03\x01\x01\x00' + >>> raw(x) + '0\r\x02\x01\x07\x04\x03egg0\x03\x01\x01\x00' + >>> xx,remain = BERcodec_Object.dec(_) + >>> xx.show() + # ASN1_SEQUENCE: + + + # ASN1_SEQUENCE: + + + >>> remain + '' + +By default, decoding is done using the ``Universal`` class, which means objects defined in the ``Context`` class will not be decoded. There is a good reason for that: the decoding depends on the context! + +:: + + >>> cert=""" + ... MIIF5jCCA86gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMC + ... VVMxHTAbBgNVBAoTFEFPTCBUaW1lIFdhcm5lciBJbmMuMRwwGgYDVQQLExNB + ... bWVyaWNhIE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIg + ... Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyOTA2MDAw + ... MFoXDTM3MDkyODIzNDMwMFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRB + ... T0wgVGltZSBXYXJuZXIgSW5jLjEcMBoGA1UECxMTQW1lcmljYSBPbmxpbmUg + ... SW5jLjE3MDUGA1UEAxMuQU9MIFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNh + ... dGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC + ... ggIBALQ3WggWmRToVbEbJGv8x4vmh6mJ7ouZzU9AhqS2TcnZsdw8TQ2FTBVs + ... RotSeJ/4I/1n9SQ6aF3Q92RhQVSji6UI0ilbm2BPJoPRYxJWSXakFsKlnUWs + ... i4SVqBax7J/qJBrvuVdcmiQhLE0OcR+mrF1FdAOYxFSMFkpBd4aVdQxHAWZg + ... /BXxD+r1FHjHDtdugRxev17nOirYlxcwfACtCJ0zr7iZYYCLqJV+FNwSbKTQ + ... 2O9ASQI2+W6p1h2WVgSysy0WVoaP2SBXgM1nEG2wTPDaRrbqJS5Gr42whTg0 + ... ixQmgiusrpkLjhTXUr2eacOGAgvqdnUxCc4zGSGFQ+aJLZ8lN2fxI2rSAG2X + ... +Z/nKcrdH9cG6rjJuQkhn8g/BsXS6RJGAE57COtCPStIbp1n3UsC5ETzkxml + ... J85per5n0/xQpCyrw2u544BMzwVhSyvcG7mm0tCq9Stz+86QNZ8MUhy/XCFh + ... EVsVS6kkUfykXPcXnbDS+gfpj1bkGoxoigTTfFrjnqKhynFbotSg5ymFXQNo + ... Kk/SBtc9+cMDLz9l+WceR0DTYw/j1Y75hauXTLPXJuuWCpTehTacyH+BCQJJ + ... Kg71ZDIMgtG6aoIbs0t0EfOMd9afv9w3pKdVBC/UMejTRrkDfNoSTllkt1Ex + ... MVCgyhwn2RAurda9EGYrw7AiShJbAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMB + ... Af8wHQYDVR0OBBYEFE9pbQN+nZ8HGEO8txBO1b+pxCAoMB8GA1UdIwQYMBaA + ... FE9pbQN+nZ8HGEO8txBO1b+pxCAoMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG + ... 9w0BAQUFAAOCAgEAO/Ouyuguh4X7ZVnnrREUpVe8WJ8kEle7+z802u6teio0 + ... cnAxa8cZmIDJgt43d15Ui47y6mdPyXSEkVYJ1eV6moG2gcKtNuTxVBFT8zRF + ... ASbI5Rq8NEQh3q0l/HYWdyGQgJhXnU7q7C+qPBR7V8F+GBRn7iTGvboVsNIY + ... vbdVgaxTwOjdaRITQrcCtQVBynlQboIOcXKTRuidDV29rs4prWPVVRaAMCf/ + ... drr3uNZK49m1+VLQTkCpx+XCMseqdiThawVQ68W/ClTluUI8JPu3B5wwn3la + ... 5uBAUhX0/Kr0VvlEl4ftDmVyXr4m+02kLQgH3thcoNyBM5kYJRF3p+v9WAks + ... mWsbivNSPxpNSGDxoPYzAlOL7SUJuA0t7Zdz7NeWH45gDtoQmy8YJPamTQr5 + ... O8t1wswvziRpyQoijlmn94IM19drNZxDAGrElWe6nEXLuA4399xOAU++CrYD + ... 062KRffaJ00psUjf5BHklka9bAI+1lHIlRcBFanyqqryvy9lG2/QuRqT9Y41 + ... xICHPpQvZuTpqP9BnHAqTyo5GJUefvthATxRCC4oGKQWDzH9OmwjkyB24f0H + ... hdFbP9IcczLd+rn4jM8Ch3qaluTtT4mNU0OrDhPAARW0eTjb/G49nlG2uBOL + ... Z8/5fNkiHfZdxRwBL5joeiQYvITX+txyW/fBOmg= + ... """.decode("base64") + >>> (dcert,remain) = BERcodec_Object.dec(cert) + Traceback (most recent call last): + File "", line 1, in ? + File "/usr/bin/scapy", line 2099, in dec + return cls.do_dec(s, context, safe) + File "/usr/bin/scapy", line 2094, in do_dec + return codec.dec(s,context,safe) + File "/usr/bin/scapy", line 2099, in dec + return cls.do_dec(s, context, safe) + File "/usr/bin/scapy", line 2218, in do_dec + o,s = BERcodec_Object.dec(s, context, safe) + File "/usr/bin/scapy", line 2099, in dec + return cls.do_dec(s, context, safe) + File "/usr/bin/scapy", line 2094, in do_dec + return codec.dec(s,context,safe) + File "/usr/bin/scapy", line 2099, in dec + return cls.do_dec(s, context, safe) + File "/usr/bin/scapy", line 2218, in do_dec + o,s = BERcodec_Object.dec(s, context, safe) + File "/usr/bin/scapy", line 2099, in dec + return cls.do_dec(s, context, safe) + File "/usr/bin/scapy", line 2092, in do_dec + raise BER_Decoding_Error("Unknown prefix [%02x] for [%r]" % (p,t), remaining=s) + BER_Decoding_Error: Unknown prefix [a0] for ['\xa0\x03\x02\x01\x02\x02\x01\x010\r\x06\t*\x86H...'] + ### Already decoded ### + [[]] + ### Remaining ### + '\xa0\x03\x02\x01\x02\x02\x01\x010\r\x06\t*\x86H\x86\xf7\r\x01\x01\x05\x05\x000\x81\x831\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x1d0\x1b\x06\x03U\x04\n\x13\x14AOL Time Warner Inc.1\x1c0\x1a\x06\x03U\x04\x0b\x13\x13America Online Inc.1705\x06\x03U\x04\x03\x13.AOL Time Warner Root Certification Authority 20\x1e\x17\r020529060000Z\x17\r370928234300Z0\x81\x831\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x1d0\x1b\x06\x03U\x04\n\x13\x14AOL Time Warner Inc.1\x1c0\x1a\x06\x03U\x04\x0b\x13\x13America Online Inc.1705\x06\x03U\x04\x03\x13.AOL Time Warner Root Certification Authority 20\x82\x02"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x02\x0f\x000\x82\x02\n\x02\x82\x02\x01\x00\xb47Z\x08\x16\x99\x14\xe8U\xb1\x1b$k\xfc\xc7\x8b\xe6\x87\xa9\x89\xee\x8b\x99\xcdO@\x86\xa4\xb6M\xc9\xd9\xb1\xdc\xd6Q\xc8\x95\x17\x01\x15\xa9\xf2\xaa\xaa\xf2\xbf/e\x1bo\xd0\xb9\x1a\x93\xf5\x8e5\xc4\x80\x87>\x94/f\xe4\xe9\xa8\xffA\x9cp*O*9\x18\x95\x1e~\xfba\x01>> (dcert,remain) = BERcodec_Object.dec(cert, context=ASN1_Class_X509) + >>> dcert.show() + # ASN1_SEQUENCE: + # ASN1_SEQUENCE: + # ASN1_X509_CONT0: + + + # ASN1_SEQUENCE: + + + # ASN1_SEQUENCE: + # ASN1_SET: + # ASN1_SEQUENCE: + + + # ASN1_SET: + # ASN1_SEQUENCE: + + + # ASN1_SET: + # ASN1_SEQUENCE: + + + # ASN1_SET: + # ASN1_SEQUENCE: + + + # ASN1_SEQUENCE: + + + # ASN1_SEQUENCE: + # ASN1_SET: + # ASN1_SEQUENCE: + + + # ASN1_SET: + # ASN1_SEQUENCE: + + + # ASN1_SET: + # ASN1_SEQUENCE: + + + # ASN1_SET: + # ASN1_SEQUENCE: + + + # ASN1_SEQUENCE: + # ASN1_SEQUENCE: + + + + # ASN1_X509_CONT3: + # ASN1_SEQUENCE: + # ASN1_SEQUENCE: + + + + # ASN1_SEQUENCE: + + + # ASN1_SEQUENCE: + + + # ASN1_SEQUENCE: + + + + # ASN1_SEQUENCE: + + + \xd6Q\xc8\x95\x17\x01\x15\xa9\xf2\xaa\xaa\xf2\xbf/e\x1bo\xd0\xb9\x1a\x93\xf5\x8e5\xc4\x80\x87>\x94/f\xe4\xe9\xa8\xffA\x9cp*O*9\x18\x95\x1e~\xfba\x01 + +ASN.1 layers +^^^^^^^^^^^^ + +While this may be nice, it's only an ASN.1 encoder/decoder. Nothing related to Scapy yet. + +ASN.1 fields +~~~~~~~~~~~~ + +Scapy provides ASN.1 fields. They will wrap ASN.1 objects and provide the necessary logic to bind a field name to the value. ASN.1 packets will be described as a tree of ASN.1 fields. Then each field name will be made available as a normal ``Packet`` object, in a flat flavor (ex: to access the version field of a SNMP packet, you don't need to know how many containers wrap it). + +Each ASN.1 field is linked to an ASN.1 object through its tag. + + +ASN.1 packets +~~~~~~~~~~~~~ + +ASN.1 packets inherit from the Packet class. Instead of a ``fields_desc`` list of fields, they define ``ASN1_codec`` and ``ASN1_root`` attributes. The first one is a codec (for example: ``ASN1_Codecs.BER``), the second one is a tree compounded with ASN.1 fields. + +A complete example: SNMP +------------------------ + +SNMP defines new ASN.1 objects. We need to define them:: + + class ASN1_Class_SNMP(ASN1_Class_UNIVERSAL): + name="SNMP" + PDU_GET = 0xa0 + PDU_NEXT = 0xa1 + PDU_RESPONSE = 0xa2 + PDU_SET = 0xa3 + PDU_TRAPv1 = 0xa4 + PDU_BULK = 0xa5 + PDU_INFORM = 0xa6 + PDU_TRAPv2 = 0xa7 + +These objects are PDU, and are in fact new names for a sequence container (this is generally the case for context objects: they are old containers with new names). This means creating the corresponding ASN.1 objects and BER codecs is simplistic:: + + class ASN1_SNMP_PDU_GET(ASN1_SEQUENCE): + tag = ASN1_Class_SNMP.PDU_GET + + class ASN1_SNMP_PDU_NEXT(ASN1_SEQUENCE): + tag = ASN1_Class_SNMP.PDU_NEXT + + # [...] + + class BERcodec_SNMP_PDU_GET(BERcodec_SEQUENCE): + tag = ASN1_Class_SNMP.PDU_GET + + class BERcodec_SNMP_PDU_NEXT(BERcodec_SEQUENCE): + tag = ASN1_Class_SNMP.PDU_NEXT + + # [...] + +Metaclasses provide the magic behind the fact that everything is automatically registered and that ASN.1 objects and BER codecs can find each other. + +The ASN.1 fields are also trivial:: + + class ASN1F_SNMP_PDU_GET(ASN1F_SEQUENCE): + ASN1_tag = ASN1_Class_SNMP.PDU_GET + + class ASN1F_SNMP_PDU_NEXT(ASN1F_SEQUENCE): + ASN1_tag = ASN1_Class_SNMP.PDU_NEXT + + # [...] + +Now, the hard part, the ASN.1 packet:: + + SNMP_error = { 0: "no_error", + 1: "too_big", + # [...] + } + + SNMP_trap_types = { 0: "cold_start", + 1: "warm_start", + # [...] + } + + class SNMPvarbind(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( ASN1F_OID("oid","1.3"), + ASN1F_field("value",ASN1_NULL(0)) + ) + + + class SNMPget(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SNMP_PDU_GET( ASN1F_INTEGER("id",0), + ASN1F_enum_INTEGER("error",0, SNMP_error), + ASN1F_INTEGER("error_index",0), + ASN1F_SEQUENCE_OF("varbindlist", [], SNMPvarbind) + ) + + class SNMPnext(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SNMP_PDU_NEXT( ASN1F_INTEGER("id",0), + ASN1F_enum_INTEGER("error",0, SNMP_error), + ASN1F_INTEGER("error_index",0), + ASN1F_SEQUENCE_OF("varbindlist", [], SNMPvarbind) + ) + # [...] + + class SNMP(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("version", 1, {0:"v1", 1:"v2c", 2:"v2", 3:"v3"}), + ASN1F_STRING("community","public"), + ASN1F_CHOICE("PDU", SNMPget(), + SNMPget, SNMPnext, SNMPresponse, SNMPset, + SNMPtrapv1, SNMPbulk, SNMPinform, SNMPtrapv2) + ) + def answers(self, other): + return ( isinstance(self.PDU, SNMPresponse) and + ( isinstance(other.PDU, SNMPget) or + isinstance(other.PDU, SNMPnext) or + isinstance(other.PDU, SNMPset) ) and + self.PDU.id == other.PDU.id ) + # [...] + bind_layers( UDP, SNMP, sport=161) + bind_layers( UDP, SNMP, dport=161) + +That wasn't that much difficult. If you think that can't be that short to implement SNMP encoding/decoding and that I may have cut too much, just look at the complete source code. + +Now, how to use it? As usual:: + + >>> a=SNMP(version=3, PDU=SNMPget(varbindlist=[SNMPvarbind(oid="1.2.3",value=5), + ... SNMPvarbind(oid="3.2.1",value="hello")])) + >>> a.show() + ###[ SNMP ]### + version= v3 + community= 'public' + \PDU\ + |###[ SNMPget ]### + | id= 0 + | error= no_error + | error_index= 0 + | \varbindlist\ + | |###[ SNMPvarbind ]### + | | oid= '1.2.3' + | | value= 5 + | |###[ SNMPvarbind ]### + | | oid= '3.2.1' + | | value= 'hello' + >>> hexdump(a) + 0000 30 2E 02 01 03 04 06 70 75 62 6C 69 63 A0 21 02 0......public.!. + 0010 01 00 02 01 00 02 01 00 30 16 30 07 06 02 2A 03 ........0.0...*. + 0020 02 01 05 30 0B 06 02 7A 01 04 05 68 65 6C 6C 6F ...0...z...hello + >>> send(IP(dst="1.2.3.4")/UDP()/SNMP()) + . + Sent 1 packets. + >>> SNMP(raw(a)).show() + ###[ SNMP ]### + version= + community= + \PDU\ + |###[ SNMPget ]### + | id= + | error= + | error_index= + | \varbindlist\ + | |###[ SNMPvarbind ]### + | | oid= + | | value= + | |###[ SNMPvarbind ]### + | | oid= + | | value= + + + +Resolving OID from a MIB +------------------------ + +About OID objects +^^^^^^^^^^^^^^^^^ + +OID objects are created with an ``ASN1_OID`` class:: + + >>> o1=ASN1_OID("2.5.29.10") + >>> o2=ASN1_OID("1.2.840.113549.1.1.1") + >>> o1,o2 + (, ) + +Loading a MIB +^^^^^^^^^^^^^ + +Scapy can parse MIB files and become aware of a mapping between an OID and its name:: + + >>> load_mib("mib/*") + >>> o1,o2 + (, ) + +The MIB files I've used are attached to this page. + +Scapy's MIB database +^^^^^^^^^^^^^^^^^^^^ + +All MIB information is stored into the conf.mib object. This object can be used to find the OID of a name + +:: + + >>> conf.mib.sha1_with_rsa_signature + '1.2.840.113549.1.1.5' + +or to resolve an OID:: + + >>> conf.mib._oidname("1.2.3.6.1.4.1.5") + 'enterprises.5' + +It is even possible to graph it:: + + >>> conf.mib._make_graph() + + +CBOR +==== + +What is CBOR? +------------- + +.. note:: + + This section provides a practical introduction to CBOR from Scapy's perspective. For the complete specification, see RFC 8949. + +CBOR (Concise Binary Object Representation) is a data format whose goal is to provide a compact, self-describing binary data interchange format based on the JSON data model. It is defined in RFC 8949 and is designed to be small in code size, reasonably small in message size, and extensible without the need for version negotiation. + +CBOR provides basic data types including: + +* **Unsigned integers** (major type 0): Non-negative integers +* **Negative integers** (major type 1): Negative integers +* **Byte strings** (major type 2): Raw binary data +* **Text strings** (major type 3): UTF-8 encoded strings +* **Arrays** (major type 4): Ordered sequences of values +* **Maps** (major type 5): Unordered key-value pairs +* **Semantic tags** (major type 6): Tagged values with additional semantics +* **Simple values and floats** (major type 7): Booleans, null, undefined, and floating-point numbers + +Each CBOR data item begins with an initial byte that encodes the major type (in the top 3 bits) and additional information (in the low 5 bits). This design allows for compact encoding while maintaining self-describing properties. + +Scapy and CBOR +-------------- + +Scapy provides a complete CBOR encoder and decoder following the same architectural pattern as the ASN.1 implementation. The CBOR engine can encode Python objects to CBOR binary format and decode CBOR data back to Python objects. It has been designed to be RFC 8949 compliant and interoperable with other CBOR implementations. + +CBOR engine +^^^^^^^^^^^ + +Scapy's CBOR engine provides classes to represent CBOR data items. The main components are: + +* ``CBOR_MajorTypes``: Defines the 8 major types (0-7) used in CBOR encoding +* ``CBOR_Object``: Base class for all CBOR value objects +* ``CBOR_Codecs``: Registry for encoding/decoding rules + +The ``CBOR_MajorTypes`` class defines tags for all major types:: + + class CBOR_MajorTypes: + name = "CBOR_MAJOR_TYPES" + UNSIGNED_INTEGER = 0 + NEGATIVE_INTEGER = 1 + BYTE_STRING = 2 + TEXT_STRING = 3 + ARRAY = 4 + MAP = 5 + TAG = 6 + SIMPLE_AND_FLOAT = 7 + +All CBOR objects are represented by Python instances that wrap raw values. They inherit from ``CBOR_Object``:: + + class CBOR_UNSIGNED_INTEGER(CBOR_Object): + tag = CBOR_MajorTypes.UNSIGNED_INTEGER + + class CBOR_TEXT_STRING(CBOR_Object): + tag = CBOR_MajorTypes.TEXT_STRING + + class CBOR_ARRAY(CBOR_Object): + tag = CBOR_MajorTypes.ARRAY + +Creating CBOR objects +^^^^^^^^^^^^^^^^^^^^^ + +CBOR objects can be easily created and composed:: + + >>> from scapy.cbor import * + >>> # Create basic types + >>> num = CBOR_UNSIGNED_INTEGER(42) + >>> text = CBOR_TEXT_STRING("Hello, CBOR!") + >>> data = CBOR_BYTE_STRING(b'\x01\x02\x03') + >>> + >>> # Create collections + >>> arr = CBOR_ARRAY([CBOR_UNSIGNED_INTEGER(1), + ... CBOR_UNSIGNED_INTEGER(2), + ... CBOR_TEXT_STRING("three")]) + >>> arr + , , ]]> + >>> + >>> # Create maps + >>> from scapy.cbor.cborcodec import CBORcodec_MAP + >>> mapping = {"name": "Alice", "age": 30, "active": True} + +Encoding and decoding +^^^^^^^^^^^^^^^^^^^^^ + +CBOR objects are encoded using their ``.enc()`` method. All codecs are referenced in the ``CBOR_Codecs`` object. The default codec is ``CBOR_Codecs.CBOR``:: + + >>> num = CBOR_UNSIGNED_INTEGER(42) + >>> encoded = bytes(num) + >>> encoded.hex() + '182a' + >>> + >>> # Decode back + >>> decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) + >>> decoded.val + 42 + >>> isinstance(decoded, CBOR_UNSIGNED_INTEGER) + True + +Encoding collections:: + + >>> from scapy.cbor.cborcodec import CBORcodec_ARRAY, CBORcodec_MAP + >>> # Encode an array + >>> encoded = CBORcodec_ARRAY.enc([1, 2, 3, 4, 5]) + >>> encoded.hex() + '850102030405' + >>> + >>> # Decode the array + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> [item.val for item in decoded.val] + [1, 2, 3, 4, 5] + >>> + >>> # Encode a map + >>> encoded = CBORcodec_MAP.enc({"x": 100, "y": 200}) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> isinstance(decoded, CBOR_MAP) + True + +Working with different types +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +CBOR supports various data types:: + + >>> # Booleans + >>> true_val = CBOR_TRUE() + >>> false_val = CBOR_FALSE() + >>> bytes(true_val).hex() + 'f5' + >>> bytes(false_val).hex() + 'f4' + >>> + >>> # Null and undefined + >>> null_val = CBOR_NULL() + >>> undef_val = CBOR_UNDEFINED() + >>> bytes(null_val).hex() + 'f6' + >>> bytes(undef_val).hex() + 'f7' + >>> + >>> # Floating point + >>> float_val = CBOR_FLOAT(3.14159) + >>> bytes(float_val).hex() + 'fb400921f9f01b866e' + >>> + >>> # Negative integers + >>> neg = CBOR_NEGATIVE_INTEGER(-100) + >>> bytes(neg).hex() + '3863' + +Complex structures +^^^^^^^^^^^^^^^^^^ + +CBOR supports nested structures:: + + >>> # Nested arrays + >>> nested = CBORcodec_ARRAY.enc([1, [2, 3], [4, [5, 6]]]) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(nested) + >>> isinstance(decoded, CBOR_ARRAY) + True + >>> + >>> # Complex maps with mixed types + >>> data = { + ... "name": "Bob", + ... "age": 25, + ... "active": True, + ... "tags": ["user", "admin"] + ... } + >>> encoded = CBORcodec_MAP.enc(data) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> len(decoded.val) + 4 + +Semantic tags +^^^^^^^^^^^^^ + +CBOR supports semantic tags (major type 6) for providing additional meaning to data items:: + + >>> # Tag 1 is for Unix epoch timestamps + >>> import time + >>> timestamp = int(time.time()) + >>> tagged = CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(timestamp))) + >>> encoded = bytes(tagged) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> decoded.val[0] # Tag number + 1 + +Interoperability +^^^^^^^^^^^^^^^^ + +Scapy's CBOR implementation is fully interoperable with other CBOR libraries. The implementation has been tested with the ``cbor2`` Python library to ensure RFC 8949 compliance:: + + >>> import cbor2 + >>> # Encode with Scapy, decode with cbor2 + >>> scapy_obj = CBOR_UNSIGNED_INTEGER(12345) + >>> scapy_encoded = bytes(scapy_obj) + >>> cbor2.loads(scapy_encoded) + 12345 + >>> + >>> # Encode with cbor2, decode with Scapy + >>> cbor2_encoded = cbor2.dumps([1, "test", True]) + >>> scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) + >>> isinstance(scapy_decoded, CBOR_ARRAY) + True + +Error handling +^^^^^^^^^^^^^^ + +Scapy provides safe decoding with error handling:: + + >>> # Safe decoding returns error objects for invalid data + >>> invalid_data = b'\xff\xff\xff' + >>> obj, remainder = CBOR_Codecs.CBOR.safedec(invalid_data) + >>> isinstance(obj, CBOR_DECODING_ERROR) + True + +Comparison with ASN.1 +^^^^^^^^^^^^^^^^^^^^^ + +While both ASN.1 and CBOR are data serialization formats, they serve different purposes: + +* **ASN.1** is designed for complex schemas with strict typing and multiple encoding rules (BER, DER, PER, etc.) +* **CBOR** is designed for simplicity and compactness, with a single self-describing encoding + +Scapy implements both following the same architectural pattern, making it easy to work with either format. CBOR's simpler structure makes it ideal for IoT, embedded systems, and web APIs, while ASN.1 is prevalent in telecommunications and cryptographic standards. + + +Automata +======== + +Scapy enables to create easily network automata. Scapy does not stick to a specific model like Moore or Mealy automata. It provides a flexible way for you to choose your way to go. + +An automaton in Scapy is deterministic. It has different states. A start state and some end and error states. There are transitions from one state to another. Transitions can be transitions on a specific condition, transitions on the reception of a specific packet or transitions on a timeout. When a transition is taken, one or more actions can be run. An action can be bound to many transitions. Parameters can be passed from states to transitions, and from transitions to states and actions. + +From a programmer's point of view, states, transitions and actions are methods from an Automaton subclass. They are decorated to provide meta-information needed in order for the automaton to work. + +First example +------------- + +Let's begin with a simple example. I take the convention to write states with capitals, but anything valid with Python syntax would work as well. + +:: + + class HelloWorld(Automaton): + @ATMT.state(initial=1) + def BEGIN(self): + print("State=BEGIN") + + @ATMT.condition(BEGIN) + def wait_for_nothing(self): + print("Wait for nothing...") + raise self.END() + + @ATMT.action(wait_for_nothing) + def on_nothing(self): + print("Action on 'nothing' condition") + + @ATMT.state(final=1) + def END(self): + print("State=END") + +In this example, we can see 3 decorators: + +* ``ATMT.state`` that is used to indicate that a method is a state, and that can + have initial, final, stop and error optional arguments set to non-zero for special states. +* ``ATMT.condition`` that indicate a method to be run when the automaton state + reaches the indicated state. The argument is the name of the method representing that state +* ``ATMT.action`` binds a method to a transition and is run when the transition is taken. + +Running this example gives the following result:: + + >>> a=HelloWorld() + >>> a.run() + State=BEGIN + Wait for nothing... + Action on 'nothing' condition + State=END + >>> a.destroy() + +This simple automaton can be described with the following graph: + +.. image:: graphics/ATMT_HelloWorld.* + +The graph can be automatically drawn from the code with:: + + >>> HelloWorld.graph() + +.. note:: An ``Automaton`` can be reset using ``restart()``. It is then possible to run it again. + +.. warning:: Remember to call ``destroy()`` once you're done using an Automaton. (especially on PyPy) + +Changing states +--------------- + +The ``ATMT.state`` decorator transforms a method into a function that returns an exception. If you raise that exception, the automaton state will be changed. If the change occurs in a transition, actions bound to this transition will be called. The parameters given to the function replacing the method will be kept and finally delivered to the method. The exception has a method action_parameters that can be called before it is raised so that it will store parameters to be delivered to all actions bound to the current transition. + +As an example, let's consider the following state:: + + @ATMT.state() + def MY_STATE(self, param1, param2): + print("state=MY_STATE. param1=%r param2=%r" % (param1, param2)) + +This state will be reached with the following code:: + + @ATMT.receive_condition(ANOTHER_STATE) + def received_ICMP(self, pkt): + if ICMP in pkt: + raise self.MY_STATE("got icmp", pkt[ICMP].type) + +Let's suppose we want to bind an action to this transition, that will also need some parameters:: + + @ATMT.action(received_ICMP) + def on_ICMP(self, icmp_type, icmp_code): + self.retaliate(icmp_type, icmp_code) + +The condition should become:: + + @ATMT.receive_condition(ANOTHER_STATE) + def received_ICMP(self, pkt): + if ICMP in pkt: + raise self.MY_STATE("got icmp", pkt[ICMP].type).action_parameters(pkt[ICMP].type, pkt[ICMP].code) + +Real example +------------ + +Here is a real example take from Scapy. It implements a TFTP client that can issue read requests. + +.. image:: graphics/ATMT_TFTP_read.* + +:: + + class TFTP_read(Automaton): + def parse_args(self, filename, server, sport = None, port=69, **kargs): + Automaton.parse_args(self, **kargs) + self.filename = filename + self.server = server + self.port = port + self.sport = sport + + def master_filter(self, pkt): + return ( IP in pkt and pkt[IP].src == self.server and UDP in pkt + and pkt[UDP].dport == self.my_tid + and (self.server_tid is None or pkt[UDP].sport == self.server_tid) ) + + # BEGIN + @ATMT.state(initial=1) + def BEGIN(self): + self.blocksize=512 + self.my_tid = self.sport or RandShort()._fix() + bind_bottom_up(UDP, TFTP, dport=self.my_tid) + self.server_tid = None + self.res = b"" + + self.l3 = IP(dst=self.server)/UDP(sport=self.my_tid, dport=self.port)/TFTP() + self.last_packet = self.l3/TFTP_RRQ(filename=self.filename, mode="octet") + self.send(self.last_packet) + self.awaiting=1 + + raise self.WAITING() + + # WAITING + @ATMT.state() + def WAITING(self): + pass + + @ATMT.receive_condition(WAITING) + def receive_data(self, pkt): + if TFTP_DATA in pkt and pkt[TFTP_DATA].block == self.awaiting: + if self.server_tid is None: + self.server_tid = pkt[UDP].sport + self.l3[UDP].dport = self.server_tid + raise self.RECEIVING(pkt) + @ATMT.action(receive_data) + def send_ack(self): + self.last_packet = self.l3 / TFTP_ACK(block = self.awaiting) + self.send(self.last_packet) + + @ATMT.receive_condition(WAITING, prio=1) + def receive_error(self, pkt): + if TFTP_ERROR in pkt: + raise self.ERROR(pkt) + + @ATMT.timeout(WAITING, 3) + def timeout_waiting(self): + raise self.WAITING() + @ATMT.action(timeout_waiting) + def retransmit_last_packet(self): + self.send(self.last_packet) + + # RECEIVED + @ATMT.state() + def RECEIVING(self, pkt): + recvd = pkt[Raw].load + self.res += recvd + self.awaiting += 1 + if len(recvd) == self.blocksize: + raise self.WAITING() + raise self.END() + + # ERROR + @ATMT.state(error=1) + def ERROR(self,pkt): + split_bottom_up(UDP, TFTP, dport=self.my_tid) + return pkt[TFTP_ERROR].summary() + + #END + @ATMT.state(final=1) + def END(self): + split_bottom_up(UDP, TFTP, dport=self.my_tid) + return self.res + +It can be run like this, for instance:: + + >>> atmt = TFTP_read("my_file", "192.168.1.128") + >>> atmt.run() + >>> atmt.destroy() + +Detailed documentation +---------------------- + +Decorators +^^^^^^^^^^ +Decorator for states +~~~~~~~~~~~~~~~~~~~~ + +States are methods decorated by the result of the ``ATMT.state`` function. It can take 4 optional parameters, ``initial``, ``final``, ``stop`` and ``error``, that, when set to ``True``, indicating that the state is an initial, final, stop or error state. + +.. note:: The ``initial`` state is called while starting the automata. The ``final`` step will tell the automata has reached its end. If you call ``atmt.stop()``, the automata will move to the ``stop`` step whatever its current state is. The ``error`` state will mark the automata as errored. If no ``stop`` state is specified, calling ``stop`` and ``forcestop`` will be equivalent. + +:: + + class Example(Automaton): + @ATMT.state(initial=1) + def BEGIN(self): + pass + + @ATMT.state() + def SOME_STATE(self): + pass + + @ATMT.state(final=1) + def END(self): + return "Result of the automaton: 42" + + @ATMT.state(stop=1) + def STOP(self): + print("SHUTTING DOWN...") + # e.g. close sockets... + + @ATMT.condition(STOP) + def is_stopping(self): + raise self.END() + + @ATMT.state(error=1) + def ERROR(self): + return "Partial result, or explanation" + # [...] + +Take for instance the TCP client: + +.. image:: graphics/ATMT_TCP_client.svg + +The ``START`` event is ``initial=1``, the ``STOP`` event is ``stop=1`` and the ``CLOSED`` event is ``final=1``. + +Decorators for transitions +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Transitions are methods decorated by the result of one of ``ATMT.condition``, ``ATMT.receive_condition``, ``ATMT.eof``, ``ATMT.timeout``, ``ATMT.timer``. They all take as argument the state method they are related to. ``ATMT.timeout`` and ``ATMT.timer`` also have a mandatory ``timeout`` parameter to provide the timeout value in seconds. The difference between ``ATMT.timeout`` and ``ATMT.timer`` is that ``ATMT.timeout`` gets triggered only once. ``ATMT.timer`` get reloaded automatically, which is useful for sending keep-alive packets. ``ATMT.condition`` and ``ATMT.receive_condition`` have an optional ``prio`` parameter so that the order in which conditions are evaluated can be forced. The default priority is 0. Transitions with the same priority level are called in an undetermined order. + +When the automaton switches to a given state, the state's method is executed. Then transitions methods are called at specific moments until one triggers a new state (something like ``raise self.MY_NEW_STATE()``). First, right after the state's method returns, the ``ATMT.condition`` decorated methods are run by growing prio. Then each time a packet is received and accepted by the master filter all ``ATMT.receive_condition`` decorated hods are called by growing prio. When a timeout is reached since the time we entered into the current space, the corresponding ``ATMT.timeout`` decorated method is called. If the socket raises an ``EOFError`` (closed) during a state, the ``ATMT.EOF`` transition is called. Otherwise it raises an exception and the automaton exits. + +:: + + class Example(Automaton): + @ATMT.state() + def WAITING(self): + pass + + @ATMT.condition(WAITING) + def it_is_raining(self): + if not self.have_umbrella: + raise self.ERROR_WET() + + @ATMT.receive_condition(WAITING, prio=1) + def it_is_ICMP(self, pkt): + if ICMP in pkt: + raise self.RECEIVED_ICMP(pkt) + + @ATMT.receive_condition(WAITING, prio=2) + def it_is_IP(self, pkt): + if IP in pkt: + raise self.RECEIVED_IP(pkt) + + @ATMT.timeout(WAITING, 10.0) + def waiting_timeout(self): + raise self.ERROR_TIMEOUT() + +Decorator for actions +~~~~~~~~~~~~~~~~~~~~~ + +Actions are methods that are decorated by the return of ``ATMT.action`` function. This function takes the transition method it is bound to as first parameter and an optional priority ``prio`` as a second parameter. The default priority is 0. An action method can be decorated many times to be bound to many transitions. + +:: + + from random import random + + class Example(Automaton): + @ATMT.state(initial=1) + def BEGIN(self): + pass + + @ATMT.state(final=1) + def END(self): + pass + + @ATMT.condition(BEGIN, prio=1) + def maybe_go_to_end(self): + if random() > 0.5: + raise self.END() + + @ATMT.condition(BEGIN, prio=2) + def certainly_go_to_end(self): + raise self.END() + + @ATMT.action(maybe_go_to_end) + def maybe_action(self): + print("We are lucky...") + + @ATMT.action(certainly_go_to_end) + def certainly_action(self): + print("We are not lucky...") + + @ATMT.action(maybe_go_to_end, prio=1) + @ATMT.action(certainly_go_to_end, prio=1) + def always_action(self): + print("This wasn't luck!...") + +The two possible outputs are:: + + >>> a=Example() + >>> a.run() + We are not lucky... + This wasn't luck!... + >>> a.run() + We are lucky... + This wasn't luck!... + >>> a.destroy() + + +.. note:: If you want to pass a parameter to an action, you can use the ``action_parameters`` function while raising the next state. + +In the following example, the ``send_copy`` action takes a parameter passed by ``is_fin``:: + + class Example(Automaton): + @ATMT.state() + def WAITING(self): + pass + + @ATMT.state() + def FIN_RECEIVED(self): + pass + + @ATMT.receive_condition(WAITING) + def is_fin(self, pkt): + if pkt[TCP].flags.F: + raise self.FIN_RECEIVED().action_parameters(pkt) + + @ATMT.action(is_fin) + def send_copy(self, pkt): + send(pkt) + + +Methods to overload +^^^^^^^^^^^^^^^^^^^ + +Two methods are hooks to be overloaded: + +* The ``parse_args()`` method is called with arguments given at ``__init__()`` and ``run()``. Use that to parametrize the behavior of your automaton. + +* The ``master_filter()`` method is called each time a packet is sniffed and decides if it is interesting for the automaton. When working on a specific protocol, this is where you will ensure the packet belongs to the connection you are being part of, so that you do not need to make all the sanity checks in each transition. + +Timer configuration +^^^^^^^^^^^^^^^^^^^ + +Some protocols allow timer configuration. In order to configure timeout values during class initialization one may use ``timer_by_name()`` method, which returns ``Timer`` object associated with the given function name:: + + class Example(Automaton): + def __init__(self, *args, **kwargs): + super(Example, self).__init__(*args, **kwargs) + timer = self.timer_by_name("waiting_timeout") + timer.set(1) + + @ATMT.state(initial=1) + def WAITING(self): + pass + + @ATMT.state(final=1) + def END(self): + pass + + @ATMT.timeout(WAITING, 10.0) + def waiting_timeout(self): + raise self.END() + +.. _pipetools: + +PipeTools +========= + +Scapy's ``pipetool`` is a smart piping system allowing to perform complex stream data management. + +The goal is to create a sequence of steps with one or several inputs and one or several outputs, with a bunch of blocks in between. +PipeTools can handle varied sources of data (and outputs) such as user input, pcap input, sniffing, wireshark... +A pipe system is implemented by manually linking all its parts. It is possible to dynamically add an element while running or set multiple drains for the same source. + +.. note:: Pipetool default objects are located inside ``scapy.pipetool`` + +Demo: sniff, anonymize, send to Wireshark +----------------------------------------- + +The following code will sniff packets on the default interface, anonymize the source and destination IP addresses and pipe it all into Wireshark. Useful when posting online examples, for instance. + +.. code-block:: python3 + + source = SniffSource(iface=conf.iface) + wire = WiresharkSink() + def transf(pkt): + if not pkt or IP not in pkt: + return pkt + pkt[IP].src = "1.1.1.1" + pkt[IP].dst = "2.2.2.2" + return pkt + + source > TransformDrain(transf) > wire + p = PipeEngine(source) + p.start() + p.wait_and_stop() + +The engine is pretty straightforward: + +.. image:: graphics/pipetool_demo.svg + +Let's run it: + +.. image:: graphics/animations/pipetool_demo.gif + +Class Types +----------- + +There are 3 different class of objects used for data management: + +- ``Sources`` +- ``Drains`` +- ``Sinks`` + +They are executed and handled by a :class:`~scapy.pipetool.PipeEngine` object. + +When running, a pipetool engine waits for any available data from the Source, and send it in the Drains linked to it. +The data then goes from Drains to Drains until it arrives in a Sink, the final state of this data. + +Let's see with a basic demo how to build a pipetool system. + +.. image:: graphics/pipetool_engine.png + +For instance, this engine was generated with this code: + +.. code:: pycon + + >>> s = CLIFeeder() + >>> s2 = CLIHighFeeder() + >>> d1 = Drain() + >>> d2 = TransformDrain(lambda x: x[::-1]) + >>> si1 = ConsoleSink() + >>> si2 = QueueSink() + >>> + >>> s > d1 + >>> d1 > si1 + >>> d1 > si2 + >>> + >>> s2 >> d1 + >>> d1 >> d2 + >>> d2 >> si1 + >>> + >>> p = PipeEngine() + >>> p.add(s) + >>> p.add(s2) + >>> p.graph(target="> the_above_image.png") + +``start()`` is used to start the :class:`~scapy.pipetool.PipeEngine`: + +.. code:: pycon + + >>> p.start() + +Now, let's play with it by sending some input data + +.. code:: pycon + + >>> s.send("foo") + >'foo' + >>> s2.send("bar") + >>'rab' + >>> s.send("i like potato") + >'i like potato' + >>> print(si2.recv(), ":", si2.recv()) + foo : i like potato + +Let's study what happens here: + +- there are **two canals** in a :class:`~scapy.pipetool.PipeEngine`, a lower one and a higher one. Some Sources write on the lower one, some on the higher one and some on both. +- most sources can be linked to any drain, on both lower and higher canals. The use of ``>`` indicates a link on the low canal, and ``>>`` on the higher one. +- when we send some data in ``s``, which is on the lower canal, as shown above, it goes through the :class:`~scapy.pipetool.Drain` then is sent to the :class:`~.scapy.pipetool.QueueSink` and to the :class:`~scapy.pipetool.ConsoleSink` +- when we send some data in ``s2``, it goes through the Drain, then the TransformDrain where the data is reversed (see the lambda), before being sent to :class:`~scapy.pipetool.ConsoleSink` only. This explains why we only have the data of the lower sources inside the QueueSink: the higher one has not been linked. + +Most of the sinks receive from both lower and upper canals. This is verifiable using the `help(ConsoleSink)` + +.. code:: pycon + + >>> help(ConsoleSink) + Help on class ConsoleSink in module scapy.pipetool: + class ConsoleSink(Sink) + | Print messages on low and high entries + | +-------+ + | >>-|--. |->> + | | print | + | >-|--' |-> + | +-------+ + | + [...] + + +Sources +^^^^^^^ + +A Source is a class that generates some data. + +There are several source types integrated with Scapy, usable as-is, but you may +also create yours. + +Default Source classes +~~~~~~~~~~~~~~~~~~~~~~ + +For any of those class, have a look at ``help([theclass])`` to get more information or the required parameters. + +- :class:`~scapy.pipetool.CLIFeeder` : a source especially used in interactive software. its ``send(data)`` generates the event data on the lower canal +- :class:`~scapy.pipetool.CLIHighFeeder` : same than CLIFeeder, but writes on the higher canal +- :class:`~scapy.pipetool.PeriodicSource` : Generate messages periodically on the low canal. +- :class:`~scapy.pipetool.AutoSource`: the default source, that must be extended to create custom sources. + +Create a custom Source +~~~~~~~~~~~~~~~~~~~~~~ + +To create a custom source, one must extend the :class:`~scapy.pipetool.AutoSource` class. + +.. note:: + + Do NOT use the default :class:`~scapy.pipetool.Source` class except if you are really sure of what you are doing: it is only used internally, and is missing some implementation. The :class:`~scapy.pipetool.AutoSource` is made to be used. + + +To send data through it, the object must call its ``self._gen_data(msg)`` or ``self._gen_high_data(msg)`` functions, which send the data into the PipeEngine. + +The Source should also (if possible), set ``self.is_exhausted`` to ``True`` when empty, to allow the clean stop of the :class:`~scapy.pipetool.PipeEngine`. If the source is infinite, it will need a force-stop (see PipeEngine below) + +For instance, here is how :class:`~scapy.pipetool.CLIHighFeeder` is implemented: + +.. code:: python3 + + class CLIFeeder(CLIFeeder): + def send(self, msg): + self._gen_high_data(msg) + def close(self): + self.is_exhausted = True + +Drains +^^^^^^ + +Default Drain classes +~~~~~~~~~~~~~~~~~~~~~ + +Drains need to be linked on the entry that you are using. It can be either on the lower one (using ``>``) or the upper one (using ``>>``). +See the basic example above. + +- :class:`~scapy.pipetool.Drain` : the most basic Drain possible. Will pass on both low and high entry if linked properly. +- :class:`~scapy.pipetool.TransformDrain` : Apply a function to messages on low and high entry +- :class:`~scapy.pipetool.UpDrain` : Repeat messages from low entry to high exit +- :class:`~scapy.pipetool.DownDrain` : Repeat messages from high entry to low exit + +Create a custom Drain +~~~~~~~~~~~~~~~~~~~~~ + +To create a custom drain, one must extend the :class:`~scapy.pipetool.Drain` class. + +A :class:`~scapy.pipetool.Drain` object will receive data from the lower canal in its ``push`` method, and from the higher canal from its ``high_push`` method. + +To send the data back into the next linked Drain / Sink, it must call the ``self._send(msg)`` or ``self._high_send(msg)`` methods. + +For instance, here is how :class:`~scapy.pipetool.TransformDrain` is implemented:: + + class TransformDrain(Drain): + def __init__(self, f, name=None): + Drain.__init__(self, name=name) + self.f = f + def push(self, msg): + self._send(self.f(msg)) + def high_push(self, msg): + self._high_send(self.f(msg)) + +Sinks +^^^^^ + +Sinks are destinations for messages. + +A :py:class:`~scapy.pipetool.Sink` receives data like a :py:class:`~scapy.pipetool.Drain`, but doesn't send any +messages after it. + +Messages on the low entry come from :py:meth:`~scapy.pipetool.Sink.push`, and messages on the +high entry come from :py:meth:`~scapy.pipetool.Sink.high_push`. + +Default Sinks classes +~~~~~~~~~~~~~~~~~~~~~ + +- :class:`~scapy.pipetool.ConsoleSink` : Print messages on low and high entries to ``stdout`` +- :class:`~scapy.pipetool.RawConsoleSink` : Print messages on low and high entries, using os.write +- :class:`~scapy.pipetool.TermSink` : Prints messages on the low and high entries, on a separate terminal +- :class:`~scapy.pipetool.QueueSink` : Collects messages on the low and high entries into a :py:class:`Queue` + +Create a custom Sink +~~~~~~~~~~~~~~~~~~~~ + +To create a custom sink, one must extend :py:class:`~scapy.pipetool.Sink` and implement +:py:meth:`~scapy.pipetool.Sink.push` and/or :py:meth:`~scapy.pipetool.Sink.high_push`. + +This is a simplified version of :py:class:`~scapy.pipetool.ConsoleSink`: + +.. code-block:: python3 + + class ConsoleSink(Sink): + def push(self, msg): + print(">%r" % msg) + def high_push(self, msg): + print(">>%r" % msg) + +Link objects +------------ + +As shown in the example, most sources can be linked to any drain, on both low +and high entry. + +The use of ``>`` indicates a link on the low entry, and ``>>`` on the high +entry. + +For example, to link ``a``, ``b`` and ``c`` on the low entries: + +.. code-block:: pycon + + >>> a = CLIFeeder() + >>> b = Drain() + >>> c = ConsoleSink() + >>> a > b > c + >>> p = PipeEngine() + >>> p.add(a) + +This wouldn't link the high entries, so something like this would do nothing: + +.. code-block:: pycon + + >>> a2 = CLIHighFeeder() + >>> a2 >> b + >>> a2.send("hello") + +Because ``b`` (:py:class:`~scapy.pipetool.Drain`) and ``c`` (:py:class:`scapy.pipetool.ConsoleSink`) are not +linked on the high entry. + +However, using a :py:class:`~scapy.pipetool.DownDrain` would bring the high messages from +:py:class:`~scapy.pipetool.CLIHighFeeder` to the lower channel: + +.. code-block:: pycon + + >>> a2 = CLIHighFeeder() + >>> b2 = DownDrain() + >>> a2 >> b2 + >>> b2 > b + >>> a2.send("hello") + +The PipeEngine class +-------------------- + +The :class:`~scapy.pipetool.PipeEngine` class is the core class of the Pipetool system. It must be initialized and passed the list of all Sources. + +There are two ways of passing sources: + +- during initialization: ``p = PipeEngine(source1, source2, ...)`` +- using the ``add(source)`` method + +A :class:`~scapy.pipetool.PipeEngine` class must be started with ``.start()`` function. It may be force-stopped with the ``.stop()``, or cleanly stopped with ``.wait_and_stop()`` + +A clean stop only works if the Sources is exhausted (has no data to send left). + +It can be printed into a graph using ``.graph()`` methods. see ``help(do_graph)`` for the list of available keyword arguments. + +Scapy advanced PipeTool objects +------------------------------- + +.. note:: Unlike the previous objects, those are not located in ``scapy.pipetool`` but in ``scapy.scapypipes`` + +Now that you know the default PipeTool objects, here are some more advanced ones, based on packet functionalities. + +- :class:`~scapy.scapypipes.SniffSource` : Read packets from an interface and send them to low exit. +- :class:`~scapy.scapypipes.RdpcapSource` : Read packets from a PCAP file send them to low exit. +- :class:`~scapy.scapypipes.InjectSink` : Packets received on low input are injected (sent) to an interface +- :class:`~scapy.scapypipes.WrpcapSink` : Packets received on low input are written to PCAP file +- :class:`~scapy.scapypipes.UDPDrain` : UDP payloads received on high entry are sent over UDP (complicated, have a look at ``help(UDPDrain)``) +- :class:`~scapy.scapypipes.FDSourceSink` : Use a file descriptor as source and sink +- :class:`~scapy.scapypipes.TCPConnectPipe`: TCP connect to addr:port and use it as source and sink +- :class:`~scapy.scapypipes.TCPListenPipe` : TCP listen on [addr:]port and use the first connection as source and sink (complicated, have a look at ``help(TCPListenPipe)``) + +Triggering +---------- + +Some special sort of Drains exists: the Trigger Drains. + +Trigger Drains are special drains, that on receiving data not only pass it by but also send a "Trigger" input, that is received and handled by the next triggered drain (if it exists). + +For example, here is a basic :class:`~scapy.scapypipes.TriggerDrain` usage: + +.. code:: pycon + + >>> a = CLIFeeder() + >>> d = TriggerDrain(lambda msg: True) # Pass messages and trigger when a condition is met + >>> d2 = TriggeredValve() + >>> s = ConsoleSink() + >>> a > d > d2 > s + >>> d ^ d2 # Link the triggers + >>> p = PipeEngine(s) + >>> p.start() + INFO: Pipe engine thread started. + >>> + >>> a.send("this will be printed") + >'this will be printed' + >>> a.send("this won't, because the valve was switched") + >>> a.send("this will, because the valve was switched again") + >'this will, because the valve was switched again' + >>> p.stop() + +Several triggering Drains exist, they are pretty explicit. It is highly recommended to check the doc using ``help([the class])`` + +- :class:`~scapy.scapypipes.TriggeredMessage` : Send a preloaded message when triggered and trigger in chain +- :class:`~scapy.scapypipes.TriggerDrain` : Pass messages and trigger when a condition is met +- :class:`~scapy.scapypipes.TriggeredValve` : Let messages alternatively pass or not, changing on trigger +- :class:`~scapy.scapypipes.TriggeredQueueingValve` : Let messages alternatively pass or queued, changing on trigger +- :class:`~scapy.scapypipes.TriggeredSwitch` : Let messages alternatively high or low, changing on trigger diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py new file mode 100644 index 00000000000..0572c57f9c7 --- /dev/null +++ b/scapy/cbor/__init__.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Package holding CBOR (Concise Binary Object Representation) related modules. +Follows the same paradigm as ASN.1 implementation. +""" + +from scapy.cbor.cbor import ( + CBOR_Error, + CBOR_Encoding_Error, + CBOR_Decoding_Error, + CBOR_BadTag_Decoding_Error, + CBOR_Codecs, + CBOR_MajorTypes, + CBOR_Object, + CBOR_UNSIGNED_INTEGER, + CBOR_NEGATIVE_INTEGER, + CBOR_BYTE_STRING, + CBOR_TEXT_STRING, + CBOR_ARRAY, + CBOR_MAP, + CBOR_SEMANTIC_TAG, + CBOR_SIMPLE_VALUE, + CBOR_FALSE, + CBOR_TRUE, + CBOR_NULL, + CBOR_UNDEFINED, + CBOR_FLOAT, + CBOR_DECODING_ERROR, +) + +from scapy.cbor.cborcodec import ( + CBORcodec_Object, + CBORcodec_UNSIGNED_INTEGER, + CBORcodec_NEGATIVE_INTEGER, + CBORcodec_BYTE_STRING, + CBORcodec_TEXT_STRING, + CBORcodec_ARRAY, + CBORcodec_MAP, + CBORcodec_SEMANTIC_TAG, + CBORcodec_SIMPLE_AND_FLOAT, +) + +__all__ = [ + # Exceptions + "CBOR_Error", + "CBOR_Encoding_Error", + "CBOR_Decoding_Error", + "CBOR_BadTag_Decoding_Error", + # Codecs + "CBOR_Codecs", + "CBOR_MajorTypes", + # Objects + "CBOR_Object", + "CBOR_UNSIGNED_INTEGER", + "CBOR_NEGATIVE_INTEGER", + "CBOR_BYTE_STRING", + "CBOR_TEXT_STRING", + "CBOR_ARRAY", + "CBOR_MAP", + "CBOR_SEMANTIC_TAG", + "CBOR_SIMPLE_VALUE", + "CBOR_FALSE", + "CBOR_TRUE", + "CBOR_NULL", + "CBOR_UNDEFINED", + "CBOR_FLOAT", + "CBOR_DECODING_ERROR", + # Codec classes + "CBORcodec_Object", + "CBORcodec_UNSIGNED_INTEGER", + "CBORcodec_NEGATIVE_INTEGER", + "CBORcodec_BYTE_STRING", + "CBORcodec_TEXT_STRING", + "CBORcodec_ARRAY", + "CBORcodec_MAP", + "CBORcodec_SEMANTIC_TAG", + "CBORcodec_SIMPLE_AND_FLOAT", +] diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py new file mode 100644 index 00000000000..76958b07cc8 --- /dev/null +++ b/scapy/cbor/cbor.py @@ -0,0 +1,357 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +CBOR (Concise Binary Object Representation) - RFC 8949 +Following the ASN.1 paradigm +""" + +from scapy.error import Scapy_Exception +from scapy.compat import plain_str +from scapy.utils import Enum_metaclass, EnumElement + +from typing import ( + Any, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + from scapy.cbor.cborcodec import CBORcodec_Object + +############## +# CBOR # +############## + + +class CBOR_Error(Scapy_Exception): + pass + + +class CBOR_Encoding_Error(CBOR_Error): + pass + + +class CBOR_Decoding_Error(CBOR_Error): + pass + + +class CBOR_BadTag_Decoding_Error(CBOR_Decoding_Error): + pass + + +class CBORCodec(EnumElement): + def register_stem(cls, stem): + # type: (Type[CBORcodec_Object[Any]]) -> None + cls._stem = stem + + def dec(cls, s, context=None): + # type: (bytes, Optional[Any]) -> CBOR_Object[Any] + return cls._stem.dec(s, context=context) # type: ignore + + def safedec(cls, s, context=None): + # type: (bytes, Optional[Any]) -> CBOR_Object[Any] + return cls._stem.safedec(s, context=context) # type: ignore + + def get_stem(cls): + # type: () -> type + return cls._stem + + +class CBOR_Codecs_metaclass(Enum_metaclass): + element_class = CBORCodec + + +class CBOR_Codecs(metaclass=CBOR_Codecs_metaclass): + CBOR = cast(CBORCodec, 1) + + +class CBORTag(EnumElement): + """Represents a CBOR major type""" + + def __init__(self, + key, # type: str + value, # type: int + codec=None # type: Optional[Dict[CBORCodec, Type[CBORcodec_Object[Any]]]] # noqa: E501 + ): + # type: (...) -> None + EnumElement.__init__(self, key, value) + if codec is None: + codec = {} + self._codec = codec + + def clone(self): + # type: () -> CBORTag + return self.__class__(self._key, self._value, self._codec) + + def register_cbor_object(self, cborobj): + # type: (Type[CBOR_Object[Any]]) -> None + self._cbor_obj = cborobj + + def cbor_object(self, val): + # type: (Any) -> CBOR_Object[Any] + if hasattr(self, "_cbor_obj"): + return self._cbor_obj(val) + raise CBOR_Error("%r does not have any assigned CBOR object" % self) + + def register(self, codecnum, codec): + # type: (CBORCodec, Type[CBORcodec_Object[Any]]) -> None + self._codec[codecnum] = codec + + def get_codec(self, codec): + # type: (Any) -> Type[CBORcodec_Object[Any]] + try: + c = self._codec[codec] + except KeyError: + raise CBOR_Error("Codec %r not found for tag %r" % (codec, self)) + return c + + +class CBOR_MajorTypes_metaclass(Enum_metaclass): + element_class = CBORTag + + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[CBOR_MajorTypes] + rdict = {} + for k, v in dct.items(): + if isinstance(v, int): + v = CBORTag(k, v) + dct[k] = v + rdict[v] = v + elif isinstance(v, CBORTag): + rdict[v] = v + dct["__rdict__"] = rdict + + ncls = cast('Type[CBOR_MajorTypes]', + type.__new__(cls, name, bases, dct)) + return ncls + + +class CBOR_MajorTypes(metaclass=CBOR_MajorTypes_metaclass): + """CBOR Major Types (RFC 8949)""" + name = "CBOR_MAJOR_TYPES" + # CBOR major types (3-bit value in the high-order 3 bits) + UNSIGNED_INTEGER = cast(CBORTag, 0) + NEGATIVE_INTEGER = cast(CBORTag, 1) + BYTE_STRING = cast(CBORTag, 2) + TEXT_STRING = cast(CBORTag, 3) + ARRAY = cast(CBORTag, 4) + MAP = cast(CBORTag, 5) + TAG = cast(CBORTag, 6) + SIMPLE_AND_FLOAT = cast(CBORTag, 7) + + +class CBOR_Object_metaclass(type): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[CBOR_Object[Any]] + c = cast( + 'Type[CBOR_Object[Any]]', + super(CBOR_Object_metaclass, cls).__new__(cls, name, bases, dct) + ) + try: + c.tag.register_cbor_object(c) + except Exception: + pass # Some objects may not have tags yet + return c + + +_K = TypeVar('_K') + + +class CBOR_Object(Generic[_K], metaclass=CBOR_Object_metaclass): + """Base class for CBOR value objects""" + tag = None # type: ignore # Subclasses must define their own tag + + def __init__(self, val): + # type: (_K) -> None + self.val = val + + def enc(self, codec=None): + # type: (Any) -> bytes + if codec is None: + codec = CBOR_Codecs.CBOR + if self.tag is None: + raise CBOR_Error("Cannot encode object without a tag") + # Pass self instead of self.val for special handling + return self.tag.get_codec(codec).enc(self) + + def __repr__(self): + # type: () -> str + return "<%s[%r]>" % (self.__class__.__name__, self.val) + + def __str__(self): + # type: () -> str + return plain_str(self.enc()) + + def __bytes__(self): + # type: () -> bytes + return self.enc() + + def strshow(self, lvl=0): + # type: (int) -> str + return (" " * lvl) + repr(self) + "\n" + + def show(self, lvl=0): + # type: (int) -> None + print(self.strshow(lvl)) + + def __eq__(self, other): + # type: (Any) -> bool + return bool(self.val == other) + + +####################### +# CBOR objects # +####################### + + +class CBOR_UNSIGNED_INTEGER(CBOR_Object[int]): + """CBOR unsigned integer (major type 0)""" + tag = CBOR_MajorTypes.UNSIGNED_INTEGER + + +class CBOR_NEGATIVE_INTEGER(CBOR_Object[int]): + """CBOR negative integer (major type 1)""" + tag = CBOR_MajorTypes.NEGATIVE_INTEGER + + +class CBOR_BYTE_STRING(CBOR_Object[bytes]): + """CBOR byte string (major type 2)""" + tag = CBOR_MajorTypes.BYTE_STRING + + +class CBOR_TEXT_STRING(CBOR_Object[str]): + """CBOR text string (major type 3)""" + tag = CBOR_MajorTypes.TEXT_STRING + + +class CBOR_ARRAY(CBOR_Object[List[Any]]): + """CBOR array (major type 4)""" + tag = CBOR_MajorTypes.ARRAY + + def strshow(self, lvl=0): + # type: (int) -> str + s = (" " * lvl) + ("# CBOR_ARRAY:") + "\n" + for o in self.val: + if hasattr(o, 'strshow'): + s += o.strshow(lvl=lvl + 1) + else: + s += (" " * (lvl + 1)) + repr(o) + "\n" + return s + + +class CBOR_MAP(CBOR_Object[Dict[Any, Any]]): + """CBOR map (major type 5)""" + tag = CBOR_MajorTypes.MAP + + def strshow(self, lvl=0): + # type: (int) -> str + s = (" " * lvl) + ("# CBOR_MAP:") + "\n" + for k, v in self.val.items(): + s += (" " * (lvl + 1)) + "Key: " + if hasattr(k, 'strshow'): + s += k.strshow(0).strip() + "\n" + else: + s += repr(k) + "\n" + s += (" " * (lvl + 1)) + "Value: " + if hasattr(v, 'strshow'): + s += v.strshow(0).strip() + "\n" + else: + s += repr(v) + "\n" + return s + + +class CBOR_SEMANTIC_TAG(CBOR_Object[Tuple[int, Any]]): + """CBOR semantic tag (major type 6)""" + tag = CBOR_MajorTypes.TAG + + +class CBOR_SIMPLE_VALUE(CBOR_Object[int]): + """CBOR simple value (major type 7)""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + +class CBOR_FALSE(CBOR_Object[bool]): + """CBOR false value""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self): + # type: () -> None + super(CBOR_FALSE, self).__init__(False) + + +class CBOR_TRUE(CBOR_Object[bool]): + """CBOR true value""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self): + # type: () -> None + super(CBOR_TRUE, self).__init__(True) + + +class CBOR_NULL(CBOR_Object[None]): + """CBOR null value""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self): + # type: () -> None + super(CBOR_NULL, self).__init__(None) + + +class CBOR_UNDEFINED(CBOR_Object[None]): + """CBOR undefined value""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self): + # type: () -> None + super(CBOR_UNDEFINED, self).__init__(None) + + +class CBOR_FLOAT(CBOR_Object[float]): + """CBOR floating-point number (major type 7)""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + +class _CBOR_ERROR(CBOR_Object[Union[bytes, CBOR_Object[Any]]]): + """CBOR decoding error wrapper""" + tag = None # type: ignore # Error objects don't have a CBOR tag + + +class CBOR_DECODING_ERROR(_CBOR_ERROR): + """CBOR decoding error object""" + + def __init__(self, val, exc=None): + # type: (Union[bytes, CBOR_Object[Any]], Optional[Exception]) -> None + CBOR_Object.__init__(self, val) + self.exc = exc + + def __repr__(self): + # type: () -> str + return "<%s[%r]{{%r}}>" % ( + self.__class__.__name__, + self.val, + self.exc and self.exc.args[0] or "" + ) + + def enc(self, codec=None): + # type: (Any) -> bytes + if isinstance(self.val, CBOR_Object): + return self.val.enc(codec) + return self.val # type: ignore diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py new file mode 100644 index 00000000000..95c5d86b995 --- /dev/null +++ b/scapy/cbor/cborcodec.py @@ -0,0 +1,702 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +CBOR Codec Implementation - RFC 8949 +Following the BER paradigm for ASN.1 +""" + +import struct +from scapy.compat import chb, orb +from scapy.cbor.cbor import ( + CBORTag, + CBOR_Codecs, + CBOR_DECODING_ERROR, + CBOR_Decoding_Error, + CBOR_Encoding_Error, + CBOR_Error, + CBOR_MajorTypes, + CBOR_Object, + _CBOR_ERROR, +) + +from typing import ( + Any, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +################## +# CBOR encoding # +################## + + +class CBOR_Exception(Exception): + pass + + +class CBOR_Codec_Encoding_Error(CBOR_Encoding_Error): + def __init__(self, + msg, # type: str + encoded=None, # type: Optional[Any] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.encoded = encoded + + +class CBOR_Codec_Decoding_Error(CBOR_Decoding_Error): + def __init__(self, + msg, # type: str + decoded=None, # type: Optional[Any] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.decoded = decoded + + +def CBOR_encode_head(major_type, value): + # type: (int, int) -> bytes + """ + Encode CBOR initial byte and additional info. + Format: 3 bits major type + 5 bits additional info + """ + if value < 24: + # Value fits in 5 bits + return chb((major_type << 5) | value) + elif value < 256: + # 1-byte value follows + return chb((major_type << 5) | 24) + chb(value) + elif value < 65536: + # 2-byte value follows + return chb((major_type << 5) | 25) + struct.pack(">H", value) + elif value < 4294967296: + # 4-byte value follows + return chb((major_type << 5) | 26) + struct.pack(">I", value) + else: + # 8-byte value follows + return chb((major_type << 5) | 27) + struct.pack(">Q", value) + + +def CBOR_decode_head(s): + # type: (bytes) -> Tuple[int, int, bytes] + """ + Decode CBOR initial byte and additional info. + Returns: (major_type, value, remaining_bytes) + """ + if not s: + raise CBOR_Codec_Decoding_Error("Empty CBOR data", remaining=s) + + initial_byte = orb(s[0]) + major_type = initial_byte >> 5 + additional_info = initial_byte & 0x1f + + if additional_info < 24: + # Value is in the additional info + return major_type, additional_info, s[1:] + elif additional_info == 24: + # 1-byte value follows + if len(s) < 2: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 1-byte value", remaining=s) + return major_type, orb(s[1]), s[2:] + elif additional_info == 25: + # 2-byte value follows + if len(s) < 3: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 2-byte value", remaining=s) + value = struct.unpack(">H", s[1:3])[0] + return major_type, value, s[3:] + elif additional_info == 26: + # 4-byte value follows + if len(s) < 5: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 4-byte value", remaining=s) + value = struct.unpack(">I", s[1:5])[0] + return major_type, value, s[5:] + elif additional_info == 27: + # 8-byte value follows + if len(s) < 9: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 8-byte value", remaining=s) + value = struct.unpack(">Q", s[1:9])[0] + return major_type, value, s[9:] + else: + raise CBOR_Codec_Decoding_Error( + "Invalid additional info: %d" % additional_info, remaining=s) + + +# [ CBOR codec classes ] # + + +class CBORcodec_metaclass(type): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[CBORcodec_Object[Any]] + c = cast('Type[CBORcodec_Object[Any]]', + super(CBORcodec_metaclass, cls).__new__(cls, name, bases, dct)) + try: + c.tag.register(c.codec, c) + except Exception: + pass # Some codecs may not have tags yet + return c + + +_K = TypeVar('_K') + + +class CBORcodec_Object(Generic[_K], metaclass=CBORcodec_metaclass): + """Base CBOR codec class""" + codec = CBOR_Codecs.CBOR + tag = CBOR_MajorTypes.UNSIGNED_INTEGER + + @classmethod + def cbor_object(cls, val): + # type: (_K) -> CBOR_Object[_K] + return cls.tag.cbor_object(val) + + @classmethod + def check_string(cls, s): + # type: (bytes) -> None + if not s: + raise CBOR_Codec_Decoding_Error( + "%s: Got empty object while expecting tag %r" % + (cls.__name__, cls.tag), remaining=s + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[Any], bytes] + """Decode CBOR data using automatic dispatch based on major type.""" + return _decode_cbor_item(s, safe=safe) + + @classmethod + def dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[Union[_CBOR_ERROR, CBOR_Object[_K]], bytes] + if not safe: + return cls.do_dec(s, context, safe) + try: + return cls.do_dec(s, context, safe) + except CBOR_Codec_Decoding_Error as e: + return CBOR_DECODING_ERROR(s, exc=e), b"" + except CBOR_Error as e: + return CBOR_DECODING_ERROR(s, exc=e), b"" + + @classmethod + def safedec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + ): + # type: (...) -> Tuple[Union[_CBOR_ERROR, CBOR_Object[_K]], bytes] + return cls.dec(s, context, safe=True) + + @classmethod + def enc(cls, s): + # type: (_K) -> bytes + raise NotImplementedError("Subclasses must implement enc") + + +CBOR_Codecs.CBOR.register_stem(CBORcodec_Object) + + +########################## +# CBORcodec objects # +########################## + + +class CBORcodec_UNSIGNED_INTEGER(CBORcodec_Object[int]): + """CBOR unsigned integer codec (major type 0)""" + tag = CBOR_MajorTypes.UNSIGNED_INTEGER + + @classmethod + def enc(cls, obj): + # type: (Union[int, CBOR_Object[int]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + i = obj.val if isinstance(obj, CBOR_Object) else obj + if i < 0: + raise CBOR_Codec_Encoding_Error( + "Cannot encode negative value as unsigned integer. " + "Use CBOR_NEGATIVE_INTEGER for negative values.") + return CBOR_encode_head(0, i) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[int], bytes] + cls.check_string(s) + major_type, value, remainder = CBOR_decode_head(s) + if major_type != 0: + raise CBOR_Codec_Decoding_Error( + "Expected major type 0 (unsigned integer), got %d" % major_type, + remaining=s) + return cls.cbor_object(value), remainder + + +class CBORcodec_NEGATIVE_INTEGER(CBORcodec_Object[int]): + """CBOR negative integer codec (major type 1)""" + tag = CBOR_MajorTypes.NEGATIVE_INTEGER + + @classmethod + def enc(cls, obj): + # type: (Union[int, CBOR_Object[int]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + i = obj.val if isinstance(obj, CBOR_Object) else obj + if i >= 0: + raise CBOR_Codec_Encoding_Error( + "Cannot encode non-negative value as negative integer. " + "Use CBOR_UNSIGNED_INTEGER for non-negative values.") + # CBOR negative integer: -1 - n + return CBOR_encode_head(1, -1 - i) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[int], bytes] + cls.check_string(s) + major_type, value, remainder = CBOR_decode_head(s) + if major_type != 1: + raise CBOR_Codec_Decoding_Error( + "Expected major type 1 (negative integer), got %d" % major_type, + remaining=s) + # Decode: -1 - n + return cls.cbor_object(-1 - value), remainder + + +class CBORcodec_BYTE_STRING(CBORcodec_Object[bytes]): + """CBOR byte string codec (major type 2)""" + tag = CBOR_MajorTypes.BYTE_STRING + + @classmethod + def enc(cls, obj): + # type: (Union[bytes, CBOR_Object[bytes]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + data = obj.val if isinstance(obj, CBOR_Object) else obj + if not isinstance(data, bytes): + data = bytes(data) + return CBOR_encode_head(2, len(data)) + data + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[bytes], bytes] + cls.check_string(s) + major_type, length, remainder = CBOR_decode_head(s) + if major_type != 2: + raise CBOR_Codec_Decoding_Error( + "Expected major type 2 (byte string), got %d" % major_type, + remaining=s) + if len(remainder) < length: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for byte string: expected %d, got %d" % + (length, len(remainder)), remaining=s) + return cls.cbor_object(remainder[:length]), remainder[length:] + + +class CBORcodec_TEXT_STRING(CBORcodec_Object[str]): + """CBOR text string codec (major type 3)""" + tag = CBOR_MajorTypes.TEXT_STRING + + @classmethod + def enc(cls, obj): + # type: (Union[str, CBOR_Object[str]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + text = obj.val if isinstance(obj, CBOR_Object) else obj + if isinstance(text, str): + text_bytes = text.encode('utf-8') + else: + text_bytes = bytes(text) + return CBOR_encode_head(3, len(text_bytes)) + text_bytes + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[str], bytes] + cls.check_string(s) + major_type, length, remainder = CBOR_decode_head(s) + if major_type != 3: + raise CBOR_Codec_Decoding_Error( + "Expected major type 3 (text string), got %d" % major_type, + remaining=s) + if len(remainder) < length: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for text string: expected %d, got %d" % + (length, len(remainder)), remaining=s) + try: + text = remainder[:length].decode('utf-8') + except UnicodeDecodeError as e: + raise CBOR_Codec_Decoding_Error( + "Invalid UTF-8 in text string: %s" % str(e), remaining=s) + return cls.cbor_object(text), remainder[length:] + + +class CBORcodec_ARRAY(CBORcodec_Object[List[Any]]): + """CBOR array codec (major type 4)""" + tag = CBOR_MajorTypes.ARRAY + + @classmethod + def enc(cls, obj): + # type: (Union[List[Any], CBOR_Object[List[Any]]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + array = obj.val if isinstance(obj, CBOR_Object) else obj + result = CBOR_encode_head(4, len(array)) + for item in array: + result += CBORcodec_Object.encode_cbor_item(item) + return result + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[List[Any]], bytes] + cls.check_string(s) + major_type, length, remainder = CBOR_decode_head(s) + if major_type != 4: + raise CBOR_Codec_Decoding_Error( + "Expected major type 4 (array), got %d" % major_type, + remaining=s) + + items = [] + for _ in range(length): + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Not enough items in array", remaining=s) + item, remainder = CBORcodec_Object.decode_cbor_item( + remainder, safe=safe) + items.append(item) + + return cls.cbor_object(items), remainder + + +class CBORcodec_MAP(CBORcodec_Object[Dict[Any, Any]]): + """CBOR map codec (major type 5)""" + tag = CBOR_MajorTypes.MAP + + @classmethod + def enc(cls, obj): + # type: (Union[Dict[Any, Any], CBOR_Object[Dict[Any, Any]]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + mapping = obj.val if isinstance(obj, CBOR_Object) else obj + result = CBOR_encode_head(5, len(mapping)) + for key, value in mapping.items(): + result += CBORcodec_Object.encode_cbor_item(key) + result += CBORcodec_Object.encode_cbor_item(value) + return result + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[Dict[Any, Any]], bytes] + cls.check_string(s) + major_type, length, remainder = CBOR_decode_head(s) + if major_type != 5: + raise CBOR_Codec_Decoding_Error( + "Expected major type 5 (map), got %d" % major_type, + remaining=s) + + mapping = {} + for _ in range(length): + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Not enough key-value pairs in map", remaining=s) + key, remainder = CBORcodec_Object.decode_cbor_item( + remainder, safe=safe) + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Map key without value", remaining=s) + value, remainder = CBORcodec_Object.decode_cbor_item( + remainder, safe=safe) + # Convert key to hashable type if it's a CBOR object + if isinstance(key, CBOR_Object): + key_val = key.val + else: + key_val = key + mapping[key_val] = value + + return cls.cbor_object(mapping), remainder + + +class CBORcodec_SEMANTIC_TAG(CBORcodec_Object[Tuple[int, Any]]): + """CBOR semantic tag codec (major type 6)""" + tag = CBOR_MajorTypes.TAG + + @classmethod + def enc(cls, obj): + # type: (Union[Tuple[int, Any], CBOR_Object[Tuple[int, Any]]]) -> bytes + from scapy.cbor.cbor import CBOR_Object + tagged_item = obj.val if isinstance(obj, CBOR_Object) else obj + tag_num, item = tagged_item + result = CBOR_encode_head(6, tag_num) + result += CBORcodec_Object.encode_cbor_item(item) + return result + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[Tuple[int, Any]], bytes] + cls.check_string(s) + major_type, tag_num, remainder = CBOR_decode_head(s) + if major_type != 6: + raise CBOR_Codec_Decoding_Error( + "Expected major type 6 (tag), got %d" % major_type, + remaining=s) + + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Tag without following item", remaining=s) + + item, remainder = CBORcodec_Object.decode_cbor_item( + remainder, safe=safe) + return cls.cbor_object((tag_num, item)), remainder + + +class CBORcodec_SIMPLE_AND_FLOAT(CBORcodec_Object[Union[int, float, bool, None]]): + """CBOR simple values and floats codec (major type 7)""" + tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + @classmethod + def enc(cls, obj): + # type: (Union[int, float, bool, None, CBOR_Object[Any]]) -> bytes + from scapy.cbor.cbor import ( + CBOR_FALSE, CBOR_TRUE, CBOR_NULL, CBOR_UNDEFINED, CBOR_Object + ) + + # Check if obj is a CBOR object instance (for special cases like UNDEFINED) + if isinstance(obj, CBOR_UNDEFINED): + return chb(0xf7) # undefined + elif isinstance(obj, CBOR_NULL): + return chb(0xf6) # null + elif isinstance(obj, CBOR_TRUE): + return chb(0xf5) # true + elif isinstance(obj, CBOR_FALSE): + return chb(0xf4) # false + elif isinstance(obj, CBOR_Object): + # For other CBOR objects, use their val attribute + val = obj.val + else: + val = obj + + if val is False: + return chb(0xf4) # false + elif val is True: + return chb(0xf5) # true + elif val is None: + return chb(0xf6) # null + elif isinstance(val, float): + # Encode as double precision (8 bytes) + return chb(0xfb) + struct.pack(">d", val) + elif isinstance(val, int) and 0 <= val <= 23: + # Simple value 0-23 + return CBOR_encode_head(7, val) + else: + raise CBOR_Codec_Encoding_Error( + "Cannot encode value as simple/float: %r" % val) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False, # type: bool + ): + # type: (...) -> Tuple[CBOR_Object[Any], bytes] + from scapy.cbor.cbor import ( + CBOR_FALSE, CBOR_TRUE, CBOR_NULL, CBOR_UNDEFINED, + CBOR_FLOAT, CBOR_SIMPLE_VALUE + ) + + cls.check_string(s) + + # For major type 7, we need special handling because additional_info + # encodes different things (simple values vs float sizes) + initial_byte = orb(s[0]) + major_type = initial_byte >> 5 + additional_info = initial_byte & 0x1f + + if major_type != 7: + raise CBOR_Codec_Decoding_Error( + "Expected major type 7 (simple/float), got %d" % major_type, + remaining=s) + + # Check for special simple values (encoded directly in additional_info) + if additional_info == 20: + return CBOR_FALSE(), s[1:] + elif additional_info == 21: + return CBOR_TRUE(), s[1:] + elif additional_info == 22: + return CBOR_NULL(), s[1:] + elif additional_info == 23: + return CBOR_UNDEFINED(), s[1:] + elif additional_info == 25: + # Half precision float (2 bytes) - IEEE 754 binary16 + if len(s) < 3: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for half float", remaining=s) + half_bytes = s[1:3] + remainder = s[3:] + # Convert IEEE 754 binary16 to binary64 (double) + half_int = struct.unpack(">H", half_bytes)[0] + sign = (half_int >> 15) & 0x1 + exponent = (half_int >> 10) & 0x1f + fraction = half_int & 0x3ff + + # Handle special cases + if exponent == 0: + if fraction == 0: + # Zero + float_val = -0.0 if sign else 0.0 + else: + # Subnormal number + float_val = ((-1) ** sign) * (fraction / 1024.0) * (2 ** -14) + elif exponent == 31: + if fraction == 0: + # Infinity + float_val = float('-inf') if sign else float('inf') + else: + # NaN + float_val = float('nan') + else: + # Normalized number + float_val = ((-1) ** sign) * (1 + fraction / 1024.0) * (2 ** (exponent - 15)) + + return CBOR_FLOAT(float_val), remainder + elif additional_info == 26: + # Single precision float (4 bytes) + if len(s) < 5: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for single float", remaining=s) + float_val = struct.unpack(">f", s[1:5])[0] + return CBOR_FLOAT(float_val), s[5:] + elif additional_info == 27: + # Double precision float (8 bytes) + if len(s) < 9: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for double float", remaining=s) + float_val = struct.unpack(">d", s[1:9])[0] + return CBOR_FLOAT(float_val), s[9:] + elif additional_info < 24: + # Simple value 0-23 + return CBOR_SIMPLE_VALUE(additional_info), s[1:] + else: + # additional_info 24 means 1-byte simple value follows + if additional_info == 24: + if len(s) < 2: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for simple value", remaining=s) + return CBOR_SIMPLE_VALUE(orb(s[1])), s[2:] + else: + raise CBOR_Codec_Decoding_Error( + "Invalid additional info for major type 7: %d" % additional_info, + remaining=s) + + +# Helper methods for encoding/decoding arbitrary CBOR items + + +def _encode_cbor_item(item): + # type: (Any) -> bytes + """Encode a Python value to CBOR bytes""" + from scapy.cbor.cbor import CBOR_Object + + if isinstance(item, CBOR_Object): + return item.enc() + elif isinstance(item, bool): + # Must check bool before int (bool is subclass of int) + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + elif isinstance(item, int): + if item >= 0: + return CBORcodec_UNSIGNED_INTEGER.enc(item) + else: + return CBORcodec_NEGATIVE_INTEGER.enc(item) + elif isinstance(item, bytes): + return CBORcodec_BYTE_STRING.enc(item) + elif isinstance(item, str): + return CBORcodec_TEXT_STRING.enc(item) + elif isinstance(item, list): + return CBORcodec_ARRAY.enc(item) + elif isinstance(item, dict): + return CBORcodec_MAP.enc(item) + elif isinstance(item, float): + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + elif item is None: + return CBORcodec_SIMPLE_AND_FLOAT.enc(None) + else: + raise CBOR_Codec_Encoding_Error( + "Cannot encode type: %s" % type(item)) + + +def _decode_cbor_item(s, safe=False): + # type: (bytes, bool) -> Tuple[CBOR_Object[Any], bytes] + """Decode CBOR bytes to a CBOR_Object""" + if not s: + raise CBOR_Codec_Decoding_Error("Empty CBOR data", remaining=s) + + initial_byte = orb(s[0]) + major_type = initial_byte >> 5 + + # Dispatch to appropriate codec based on major type + if major_type == 0: + return CBORcodec_UNSIGNED_INTEGER.dec(s, safe=safe) + elif major_type == 1: + return CBORcodec_NEGATIVE_INTEGER.dec(s, safe=safe) + elif major_type == 2: + return CBORcodec_BYTE_STRING.dec(s, safe=safe) + elif major_type == 3: + return CBORcodec_TEXT_STRING.dec(s, safe=safe) + elif major_type == 4: + return CBORcodec_ARRAY.dec(s, safe=safe) + elif major_type == 5: + return CBORcodec_MAP.dec(s, safe=safe) + elif major_type == 6: + return CBORcodec_SEMANTIC_TAG.dec(s, safe=safe) + elif major_type == 7: + return CBORcodec_SIMPLE_AND_FLOAT.dec(s, safe=safe) + else: + raise CBOR_Codec_Decoding_Error( + "Invalid major type: %d" % major_type, remaining=s) + + +# Add helper methods to CBORcodec_Object +CBORcodec_Object.encode_cbor_item = staticmethod(_encode_cbor_item) +CBORcodec_Object.decode_cbor_item = staticmethod(_decode_cbor_item) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts new file mode 100644 index 00000000000..19d0d6a9ae5 --- /dev/null +++ b/test/scapy/layers/cbor.uts @@ -0,0 +1,557 @@ +% Tests for CBOR encoding/decoding +# Following the ASN.1 test paradigm +# +# Try me with: +# bash test/run_tests -t test/scapy/layers/cbor.uts -F +# +# NOTE: Interoperability tests require cbor2 (test-only dependency): +# pip install cbor2 +# cbor2 is used ONLY in tests, NOT in the scapy CBOR implementation + +########### CBOR Basic Types ####################################### + ++ CBOR Unsigned Integer + += Encode small unsigned integer (0-23) +from scapy.cbor import * +obj = CBOR_UNSIGNED_INTEGER(0) +bytes(obj) == b'\x00' + += Encode unsigned integer with 1-byte value +obj = CBOR_UNSIGNED_INTEGER(24) +bytes(obj) == b'\x18\x18' + += Encode unsigned integer with 2-byte value +obj = CBOR_UNSIGNED_INTEGER(1000) +bytes(obj) == b'\x19\x03\xe8' + += Encode unsigned integer with 4-byte value +obj = CBOR_UNSIGNED_INTEGER(1000000) +bytes(obj) == b'\x1a\x00\x0f\x42\x40' + += Decode small unsigned integer +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x00') +obj.val == 0 and remainder == b'' + += Decode unsigned integer with 1-byte value +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x18\x18') +obj.val == 24 and remainder == b'' + += Decode unsigned integer with 2-byte value +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x19\x03\xe8') +obj.val == 1000 and remainder == b'' + ++ CBOR Negative Integer + += Encode negative integer -1 +obj = CBOR_NEGATIVE_INTEGER(-1) +bytes(obj) == b'\x20' + += Encode negative integer -10 +obj = CBOR_NEGATIVE_INTEGER(-10) +bytes(obj) == b'\x29' + += Encode negative integer -100 +obj = CBOR_NEGATIVE_INTEGER(-100) +bytes(obj) == b'\x38\x63' + += Decode negative integer -1 +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x20') +obj.val == -1 and remainder == b'' + += Decode negative integer -100 +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x38\x63') +obj.val == -100 and remainder == b'' + ++ CBOR Byte String + += Encode empty byte string +obj = CBOR_BYTE_STRING(b'') +bytes(obj) == b'\x40' + += Encode byte string +obj = CBOR_BYTE_STRING(b'hello') +bytes(obj) == b'\x45hello' + += Decode empty byte string +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x40') +obj.val == b'' and remainder == b'' + += Decode byte string +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x45hello') +obj.val == b'hello' and remainder == b'' + ++ CBOR Text String + += Encode empty text string +obj = CBOR_TEXT_STRING('') +bytes(obj) == b'\x60' + += Encode text string +obj = CBOR_TEXT_STRING('hello') +bytes(obj) == b'\x65hello' + += Encode UTF-8 text string +obj = CBOR_TEXT_STRING('café') +bytes(obj) == b'\x65caf\xc3\xa9' + += Decode empty text string +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x60') +obj.val == '' and remainder == b'' + += Decode text string +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x65hello') +obj.val == 'hello' and remainder == b'' + += Decode UTF-8 text string +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x65caf\xc3\xa9') +obj.val == 'café' and remainder == b'' + ++ CBOR Simple Values + += Encode false +obj = CBOR_FALSE() +bytes(obj) == b'\xf4' + += Encode true +obj = CBOR_TRUE() +bytes(obj) == b'\xf5' + += Encode null +obj = CBOR_NULL() +bytes(obj) == b'\xf6' + += Encode undefined +obj = CBOR_UNDEFINED() +bytes(obj) == b'\xf7' + += Decode false +obj, remainder = CBOR_Codecs.CBOR.dec(b'\xf4') +isinstance(obj, CBOR_FALSE) and obj.val is False and remainder == b'' + += Decode true +obj, remainder = CBOR_Codecs.CBOR.dec(b'\xf5') +isinstance(obj, CBOR_TRUE) and obj.val is True and remainder == b'' + += Decode null +obj, remainder = CBOR_Codecs.CBOR.dec(b'\xf6') +isinstance(obj, CBOR_NULL) and obj.val is None and remainder == b'' + += Decode undefined +obj, remainder = CBOR_Codecs.CBOR.dec(b'\xf7') +isinstance(obj, CBOR_UNDEFINED) and remainder == b'' + ++ CBOR Float + += Encode double precision float +obj = CBOR_FLOAT(1.5) +bytes(obj) == b'\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00' + += Decode double precision float +obj, remainder = CBOR_Codecs.CBOR.dec(b'\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00') +abs(obj.val - 1.5) < 0.0001 and remainder == b'' + ++ CBOR Array + += Encode empty array +obj = CBOR_ARRAY([]) +bytes(obj) == b'\x80' + += Encode array with integers +from scapy.cbor.cborcodec import CBORcodec_ARRAY +obj = CBOR_ARRAY([CBOR_UNSIGNED_INTEGER(1), CBOR_UNSIGNED_INTEGER(2), CBOR_UNSIGNED_INTEGER(3)]) +bytes(obj) == b'\x83\x01\x02\x03' + += Encode array with Python integers +result = CBORcodec_ARRAY.enc([1, 2, 3]) +result == b'\x83\x01\x02\x03' + += Decode empty array +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x80') +isinstance(obj, CBOR_ARRAY) and obj.val == [] and remainder == b'' + += Decode array with integers +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x83\x01\x02\x03') +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 3 + += Decode nested array +obj, remainder = CBOR_Codecs.CBOR.dec(b'\x82\x01\x82\x02\x03') +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 2 + ++ CBOR Map + += Encode empty map +obj = CBOR_MAP({}) +bytes(obj) == b'\xa0' + += Encode map with string keys +from scapy.cbor.cborcodec import CBORcodec_MAP +result = CBORcodec_MAP.enc({"a": 1, "b": 2}) +result == b'\xa2\x61a\x01\x61b\x02' or result == b'\xa2\x61b\x02\x61a\x01' + += Decode empty map +obj, remainder = CBOR_Codecs.CBOR.dec(b'\xa0') +isinstance(obj, CBOR_MAP) and obj.val == {} and remainder == b'' + += Decode map with integer values +obj, remainder = CBOR_Codecs.CBOR.dec(b'\xa2\x61a\x01\x61b\x02') +isinstance(obj, CBOR_MAP) and len(obj.val) == 2 + ++ CBOR Semantic Tag + += Encode semantic tag (datetime) +obj = CBOR_SEMANTIC_TAG((0, CBOR_TEXT_STRING("2013-03-21T20:04:00Z"))) +bytes(obj) == b'\xc0\x74' + b'2013-03-21T20:04:00Z' + += Decode semantic tag +obj, remainder = CBOR_Codecs.CBOR.dec(b'\xc0\x74' + b'2013-03-21T20:04:00Z') +isinstance(obj, CBOR_SEMANTIC_TAG) and obj.val[0] == 0 and remainder == b'' + ++ CBOR Roundtrip Tests + += Roundtrip unsigned integer +original = CBOR_UNSIGNED_INTEGER(42) +encoded = bytes(original) +decoded, _ = CBOR_Codecs.CBOR.dec(encoded) +decoded.val == original.val + += Roundtrip negative integer +original = CBOR_NEGATIVE_INTEGER(-42) +encoded = bytes(original) +decoded, _ = CBOR_Codecs.CBOR.dec(encoded) +decoded.val == original.val + += Roundtrip byte string +original = CBOR_BYTE_STRING(b'test data') +encoded = bytes(original) +decoded, _ = CBOR_Codecs.CBOR.dec(encoded) +decoded.val == original.val + += Roundtrip text string +original = CBOR_TEXT_STRING('test string') +encoded = bytes(original) +decoded, _ = CBOR_Codecs.CBOR.dec(encoded) +decoded.val == original.val + += Roundtrip array +from scapy.cbor.cborcodec import CBORcodec_ARRAY +encoded = CBORcodec_ARRAY.enc([1, 2, 3, 4, 5]) +decoded, _ = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_ARRAY) and len(decoded.val) == 5 + += Roundtrip map +from scapy.cbor.cborcodec import CBORcodec_MAP +encoded = CBORcodec_MAP.enc({"x": 100, "y": 200}) +decoded, _ = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_MAP) and len(decoded.val) == 2 + ++ CBOR Complex Structures + += Encode nested structure +from scapy.cbor.cborcodec import CBORcodec_MAP +input_dict = { + "name": "John", + "age": 30, + "active": True +} +encoded = CBORcodec_MAP.enc(input_dict) +len(encoded) > 0 + += Decode nested structure +encoded_data = b'\xa3\x64name\x64John\x63age\x18\x1e\x66active\xf5' +obj, remainder = CBOR_Codecs.CBOR.dec(encoded_data) +isinstance(obj, CBOR_MAP) and remainder == b'' + ++ CBOR Error Handling + += Safe decode with invalid data +obj, remainder = CBOR_Codecs.CBOR.safedec(b'\xff\xff\xff') +isinstance(obj, CBOR_DECODING_ERROR) + += Decode with insufficient bytes for length +try: + obj, remainder = CBOR_Codecs.CBOR.dec(b'\x18') + False +except: + True + += Decode byte string with insufficient data +try: + obj, remainder = CBOR_Codecs.CBOR.dec(b'\x45hel') + False +except: + True + +########### CBOR Interoperability Tests with cbor2 ################# +# These tests verify interoperability between scapy's CBOR implementation +# and the standard cbor2 library. cbor2 is ONLY used in tests, not in +# the scapy implementation. +# +# NOTE: These tests require cbor2 to be installed: pip install cbor2 + ++ CBOR Interoperability - Basic Types (Scapy encode, cbor2 decode) + += Check cbor2 availability +try: + import cbor2 + cbor2_available = True +except ImportError: + cbor2_available = False + +cbor2_available + += Interop: Scapy encode unsigned integer, cbor2 decode +import cbor2 +obj = CBOR_UNSIGNED_INTEGER(42) +encoded = bytes(obj) +decoded = cbor2.loads(encoded) +decoded == 42 + += Interop: Scapy encode negative integer, cbor2 decode +obj = CBOR_NEGATIVE_INTEGER(-100) +encoded = bytes(obj) +decoded = cbor2.loads(encoded) +decoded == -100 + += Interop: Scapy encode text string, cbor2 decode +obj = CBOR_TEXT_STRING("Hello, World!") +encoded = bytes(obj) +decoded = cbor2.loads(encoded) +decoded == "Hello, World!" + += Interop: Scapy encode UTF-8 text string, cbor2 decode +obj = CBOR_TEXT_STRING("Café ☕") +encoded = bytes(obj) +decoded = cbor2.loads(encoded) +decoded == "Café ☕" + += Interop: Scapy encode byte string, cbor2 decode +obj = CBOR_BYTE_STRING(b'\x01\x02\x03\x04\x05') +encoded = bytes(obj) +decoded = cbor2.loads(encoded) +decoded == b'\x01\x02\x03\x04\x05' + += Interop: Scapy encode true, cbor2 decode +obj = CBOR_TRUE() +encoded = bytes(obj) +decoded = cbor2.loads(encoded) +decoded is True + += Interop: Scapy encode false, cbor2 decode +obj = CBOR_FALSE() +encoded = bytes(obj) +decoded = cbor2.loads(encoded) +decoded is False + += Interop: Scapy encode null, cbor2 decode +obj = CBOR_NULL() +encoded = bytes(obj) +decoded = cbor2.loads(encoded) +decoded is None + += Interop: Scapy encode undefined, cbor2 decode +obj = CBOR_UNDEFINED() +encoded = bytes(obj) +decoded = cbor2.loads(encoded) +from cbor2 import undefined +decoded is undefined + += Interop: Scapy encode float, cbor2 decode +obj = CBOR_FLOAT(3.14159) +encoded = bytes(obj) +decoded = cbor2.loads(encoded) +abs(decoded - 3.14159) < 0.0001 + ++ CBOR Interoperability - Collections (Scapy encode, cbor2 decode) + += Interop: Scapy encode array, cbor2 decode +from scapy.cbor.cborcodec import CBORcodec_ARRAY +encoded = CBORcodec_ARRAY.enc([1, 2, 3, 4, 5]) +decoded = cbor2.loads(encoded) +decoded == [1, 2, 3, 4, 5] + += Interop: Scapy encode nested array, cbor2 decode +encoded = CBORcodec_ARRAY.enc([1, [2, 3], [4, [5, 6]]]) +decoded = cbor2.loads(encoded) +decoded == [1, [2, 3], [4, [5, 6]]] + += Interop: Scapy encode map, cbor2 decode +from scapy.cbor.cborcodec import CBORcodec_MAP +encoded = CBORcodec_MAP.enc({"a": 1, "b": 2, "c": 3}) +decoded = cbor2.loads(encoded) +decoded == {"a": 1, "b": 2, "c": 3} + += Interop: Scapy encode complex map, cbor2 decode +data = {"name": "Alice", "age": 30, "active": True, "tags": ["user", "admin"]} +encoded = CBORcodec_MAP.enc(data) +decoded = cbor2.loads(encoded) +decoded == data + += Interop: Scapy encode mixed array, cbor2 decode +encoded = CBORcodec_ARRAY.enc([42, "hello", True, None, 3.14, [1, 2]]) +decoded = cbor2.loads(encoded) +len(decoded) == 6 and decoded[0] == 42 and decoded[1] == "hello" + ++ CBOR Interoperability - Basic Types (cbor2 encode, Scapy decode) + += Interop: cbor2 encode unsigned integer, Scapy decode +encoded = cbor2.dumps(42) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +obj.val == 42 and isinstance(obj, CBOR_UNSIGNED_INTEGER) + += Interop: cbor2 encode negative integer, Scapy decode +encoded = cbor2.dumps(-100) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +obj.val == -100 and isinstance(obj, CBOR_NEGATIVE_INTEGER) + += Interop: cbor2 encode text string, Scapy decode +encoded = cbor2.dumps("Hello, World!") +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +obj.val == "Hello, World!" and isinstance(obj, CBOR_TEXT_STRING) + += Interop: cbor2 encode UTF-8 text string, Scapy decode +encoded = cbor2.dumps("Café ☕") +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +obj.val == "Café ☕" and isinstance(obj, CBOR_TEXT_STRING) + += Interop: cbor2 encode byte string, Scapy decode +encoded = cbor2.dumps(b'\x01\x02\x03\x04\x05') +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +obj.val == b'\x01\x02\x03\x04\x05' and isinstance(obj, CBOR_BYTE_STRING) + += Interop: cbor2 encode true, Scapy decode +encoded = cbor2.dumps(True) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +obj.val is True and isinstance(obj, CBOR_TRUE) + += Interop: cbor2 encode false, Scapy decode +encoded = cbor2.dumps(False) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +obj.val is False and isinstance(obj, CBOR_FALSE) + += Interop: cbor2 encode null, Scapy decode +encoded = cbor2.dumps(None) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +obj.val is None and isinstance(obj, CBOR_NULL) + += Interop: cbor2 encode undefined, Scapy decode +from cbor2 import CBORSimpleValue, undefined +encoded = cbor2.dumps(undefined) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(obj, CBOR_UNDEFINED) + += Interop: cbor2 encode float, Scapy decode +encoded = cbor2.dumps(3.14159) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +abs(obj.val - 3.14159) < 0.0001 and isinstance(obj, CBOR_FLOAT) + ++ CBOR Interoperability - Collections (cbor2 encode, Scapy decode) + += Interop: cbor2 encode array, Scapy decode +encoded = cbor2.dumps([1, 2, 3, 4, 5]) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 5 + += Interop: cbor2 encode nested array, Scapy decode +encoded = cbor2.dumps([1, [2, 3], [4, [5, 6]]]) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 3 + += Interop: cbor2 encode map, Scapy decode +encoded = cbor2.dumps({"a": 1, "b": 2, "c": 3}) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(obj, CBOR_MAP) and len(obj.val) == 3 + += Interop: cbor2 encode complex map, Scapy decode +data = {"name": "Alice", "age": 30, "active": True} +encoded = cbor2.dumps(data) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(obj, CBOR_MAP) and "name" in obj.val + += Interop: cbor2 encode mixed array, Scapy decode +encoded = cbor2.dumps([42, "hello", True, None, 3.14]) +obj, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 5 + ++ CBOR Interoperability - Roundtrip Tests + += Interop roundtrip: integer through cbor2 +original_val = 12345 +scapy_obj = CBOR_UNSIGNED_INTEGER(original_val) +scapy_encoded = bytes(scapy_obj) +cbor2_decoded = cbor2.loads(scapy_encoded) +cbor2_encoded = cbor2.dumps(cbor2_decoded) +scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) +scapy_decoded.val == original_val + += Interop roundtrip: string through cbor2 +original_val = "Test String 测试" +scapy_obj = CBOR_TEXT_STRING(original_val) +scapy_encoded = bytes(scapy_obj) +cbor2_decoded = cbor2.loads(scapy_encoded) +cbor2_encoded = cbor2.dumps(cbor2_decoded) +scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) +scapy_decoded.val == original_val + += Interop roundtrip: array through cbor2 +original_val = [1, "two", 3.0, True, None] +scapy_encoded = CBORcodec_ARRAY.enc(original_val) +cbor2_decoded = cbor2.loads(scapy_encoded) +cbor2_encoded = cbor2.dumps(cbor2_decoded) +scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) +isinstance(scapy_decoded, CBOR_ARRAY) and len(scapy_decoded.val) == 5 + += Interop roundtrip: map through cbor2 +original_val = {"int": 42, "str": "value", "bool": True, "null": None} +scapy_encoded = CBORcodec_MAP.enc(original_val) +cbor2_decoded = cbor2.loads(scapy_encoded) +cbor2_encoded = cbor2.dumps(cbor2_decoded) +scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) +isinstance(scapy_decoded, CBOR_MAP) and len(scapy_decoded.val) == 4 + ++ CBOR Interoperability - Edge Cases + += Interop: Large unsigned integer +large_int = 18446744073709551615 # 2^64 - 1 +encoded = cbor2.dumps(large_int) +obj, _ = CBOR_Codecs.CBOR.dec(encoded) +obj.val == large_int + += Interop: Very negative integer +neg_int = -18446744073709551616 # -(2^64) +encoded = cbor2.dumps(neg_int) +obj, _ = CBOR_Codecs.CBOR.dec(encoded) +obj.val == neg_int + += Interop: Empty collections +empty_array = cbor2.dumps([]) +obj1, _ = CBOR_Codecs.CBOR.dec(empty_array) +empty_map = cbor2.dumps({}) +obj2, _ = CBOR_Codecs.CBOR.dec(empty_map) +isinstance(obj1, CBOR_ARRAY) and len(obj1.val) == 0 and isinstance(obj2, CBOR_MAP) and len(obj2.val) == 0 + += Interop: Deeply nested structure +deep = {"level1": {"level2": {"level3": {"level4": [1, 2, 3]}}}} +encoded = cbor2.dumps(deep) +obj, _ = CBOR_Codecs.CBOR.dec(encoded) +isinstance(obj, CBOR_MAP) + += Interop: Special float values (infinity) +import math +pos_inf_encoded = cbor2.dumps(math.inf) +pos_inf_obj, _ = CBOR_Codecs.CBOR.dec(pos_inf_encoded) +neg_inf_encoded = cbor2.dumps(-math.inf) +neg_inf_obj, _ = CBOR_Codecs.CBOR.dec(neg_inf_encoded) +math.isinf(pos_inf_obj.val) and math.isinf(neg_inf_obj.val) + += Interop: Special float value (NaN) +nan_encoded = cbor2.dumps(math.nan) +nan_obj, _ = CBOR_Codecs.CBOR.dec(nan_encoded) +math.isnan(nan_obj.val) + += Interop: Zero values +zero_int = cbor2.dumps(0) +zero_float = cbor2.dumps(0.0) +obj1, _ = CBOR_Codecs.CBOR.dec(zero_int) +obj2, _ = CBOR_Codecs.CBOR.dec(zero_float) +obj1.val == 0 and obj2.val == 0.0 diff --git a/tox.ini b/tox.ini index c3a06ac726a..13d909d31df 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,7 @@ deps = cryptography coverage[toml] python-can + cbor2 # disabled on windows because they require c++ dependencies # brotli 1.1.0 broken https://github.com/google/brotli/issues/1072 brotli < 1.1.0 ; sys_platform != 'win32' From 055626fecdca2230ebd84c59482c066fe555a405 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 11 Feb 2026 21:40:06 +0100 Subject: [PATCH 2/6] Reorder imports in `cbor.py` and `cborcodec.py` for better organization; remove trailing whitespaces. --- scapy/cbor/cbor.py | 13 ++++---- scapy/cbor/cborcodec.py | 69 +++++++++++++++++++++-------------------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 76958b07cc8..80fbcb56610 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -7,10 +7,6 @@ Following the ASN.1 paradigm """ -from scapy.error import Scapy_Exception -from scapy.compat import plain_str -from scapy.utils import Enum_metaclass, EnumElement - from typing import ( Any, Dict, @@ -25,9 +21,14 @@ TYPE_CHECKING, ) +from scapy.compat import plain_str +from scapy.error import Scapy_Exception +from scapy.utils import Enum_metaclass, EnumElement + if TYPE_CHECKING: from scapy.cbor.cborcodec import CBORcodec_Object + ############## # CBOR # ############## @@ -77,7 +78,7 @@ class CBOR_Codecs(metaclass=CBOR_Codecs_metaclass): class CBORTag(EnumElement): """Represents a CBOR major type""" - + def __init__(self, key, # type: str value, # type: int @@ -336,7 +337,7 @@ class _CBOR_ERROR(CBOR_Object[Union[bytes, CBOR_Object[Any]]]): class CBOR_DECODING_ERROR(_CBOR_ERROR): """CBOR decoding error object""" - + def __init__(self, val, exc=None): # type: (Union[bytes, CBOR_Object[Any]], Optional[Exception]) -> None CBOR_Object.__init__(self, val) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 95c5d86b995..18d342993cc 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -8,19 +8,6 @@ """ import struct -from scapy.compat import chb, orb -from scapy.cbor.cbor import ( - CBORTag, - CBOR_Codecs, - CBOR_DECODING_ERROR, - CBOR_Decoding_Error, - CBOR_Encoding_Error, - CBOR_Error, - CBOR_MajorTypes, - CBOR_Object, - _CBOR_ERROR, -) - from typing import ( Any, Dict, @@ -34,6 +21,19 @@ cast, ) +from scapy.cbor.cbor import ( + CBOR_Codecs, + CBOR_DECODING_ERROR, + CBOR_Decoding_Error, + CBOR_Encoding_Error, + CBOR_Error, + CBOR_MajorTypes, + CBOR_Object, + _CBOR_ERROR, +) +from scapy.compat import chb, orb + + ################## # CBOR encoding # ################## @@ -98,11 +98,11 @@ def CBOR_decode_head(s): """ if not s: raise CBOR_Codec_Decoding_Error("Empty CBOR data", remaining=s) - + initial_byte = orb(s[0]) major_type = initial_byte >> 5 additional_info = initial_byte & 0x1f - + if additional_info < 24: # Value is in the additional info return major_type, additional_info, s[1:] @@ -391,7 +391,7 @@ def do_dec(cls, raise CBOR_Codec_Decoding_Error( "Expected major type 4 (array), got %d" % major_type, remaining=s) - + items = [] for _ in range(length): if not remainder: @@ -400,7 +400,7 @@ def do_dec(cls, item, remainder = CBORcodec_Object.decode_cbor_item( remainder, safe=safe) items.append(item) - + return cls.cbor_object(items), remainder @@ -432,7 +432,7 @@ def do_dec(cls, raise CBOR_Codec_Decoding_Error( "Expected major type 5 (map), got %d" % major_type, remaining=s) - + mapping = {} for _ in range(length): if not remainder: @@ -451,7 +451,7 @@ def do_dec(cls, else: key_val = key mapping[key_val] = value - + return cls.cbor_object(mapping), remainder @@ -482,11 +482,11 @@ def do_dec(cls, raise CBOR_Codec_Decoding_Error( "Expected major type 6 (tag), got %d" % major_type, remaining=s) - + if not remainder: raise CBOR_Codec_Decoding_Error( "Tag without following item", remaining=s) - + item, remainder = CBORcodec_Object.decode_cbor_item( remainder, safe=safe) return cls.cbor_object((tag_num, item)), remainder @@ -502,7 +502,7 @@ def enc(cls, obj): from scapy.cbor.cbor import ( CBOR_FALSE, CBOR_TRUE, CBOR_NULL, CBOR_UNDEFINED, CBOR_Object ) - + # Check if obj is a CBOR object instance (for special cases like UNDEFINED) if isinstance(obj, CBOR_UNDEFINED): return chb(0xf7) # undefined @@ -517,7 +517,7 @@ def enc(cls, obj): val = obj.val else: val = obj - + if val is False: return chb(0xf4) # false elif val is True: @@ -545,20 +545,20 @@ def do_dec(cls, CBOR_FALSE, CBOR_TRUE, CBOR_NULL, CBOR_UNDEFINED, CBOR_FLOAT, CBOR_SIMPLE_VALUE ) - + cls.check_string(s) - + # For major type 7, we need special handling because additional_info # encodes different things (simple values vs float sizes) initial_byte = orb(s[0]) major_type = initial_byte >> 5 additional_info = initial_byte & 0x1f - + if major_type != 7: raise CBOR_Codec_Decoding_Error( "Expected major type 7 (simple/float), got %d" % major_type, remaining=s) - + # Check for special simple values (encoded directly in additional_info) if additional_info == 20: return CBOR_FALSE(), s[1:] @@ -580,7 +580,7 @@ def do_dec(cls, sign = (half_int >> 15) & 0x1 exponent = (half_int >> 10) & 0x1f fraction = half_int & 0x3ff - + # Handle special cases if exponent == 0: if fraction == 0: @@ -598,8 +598,11 @@ def do_dec(cls, float_val = float('nan') else: # Normalized number - float_val = ((-1) ** sign) * (1 + fraction / 1024.0) * (2 ** (exponent - 15)) - + float_val = ( + ((-1) ** sign) * + (1 + fraction / 1024.0) * + (2 ** (exponent - 15))) + return CBOR_FLOAT(float_val), remainder elif additional_info == 26: # Single precision float (4 bytes) @@ -638,7 +641,7 @@ def _encode_cbor_item(item): # type: (Any) -> bytes """Encode a Python value to CBOR bytes""" from scapy.cbor.cbor import CBOR_Object - + if isinstance(item, CBOR_Object): return item.enc() elif isinstance(item, bool): @@ -671,10 +674,10 @@ def _decode_cbor_item(s, safe=False): """Decode CBOR bytes to a CBOR_Object""" if not s: raise CBOR_Codec_Decoding_Error("Empty CBOR data", remaining=s) - + initial_byte = orb(s[0]) major_type = initial_byte >> 5 - + # Dispatch to appropriate codec based on major type if major_type == 0: return CBORcodec_UNSIGNED_INTEGER.dec(s, safe=safe) From 6f45fbe5a2f4770c8d7a019b503f0217b9fa19f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:42:50 +0000 Subject: [PATCH 3/6] Add adapted tests from PR #4875 for CBOR encoding edge cases Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- test/scapy/layers/cbor.uts | 224 +++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 19d0d6a9ae5..97322e388c3 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -555,3 +555,227 @@ zero_float = cbor2.dumps(0.0) obj1, _ = CBOR_Codecs.CBOR.dec(zero_int) obj2, _ = CBOR_Codecs.CBOR.dec(zero_float) obj1.val == 0 and obj2.val == 0.0 + +########### Additional Tests Adapted from PR #4875 ################### +# These tests verify specific encoding sizes and edge cases + ++ CBOR Encoding Sizes - Unsigned Integers + += uint encoding size 0 (argument in initial byte) +obj = CBOR_UNSIGNED_INTEGER(0x12) +data = bytes(obj) +data == bytes.fromhex('12') + += uint encoding size 1 (1-byte argument follows) +obj = CBOR_UNSIGNED_INTEGER(0x34) +data = bytes(obj) +data == bytes.fromhex('1834') + += uint encoding size 2 (2-byte argument follows) +obj = CBOR_UNSIGNED_INTEGER(0x1234) +data = bytes(obj) +data == bytes.fromhex('191234') + += uint encoding size 4 (4-byte argument follows) +obj = CBOR_UNSIGNED_INTEGER(0x12345678) +data = bytes(obj) +data == bytes.fromhex('1a12345678') + += uint encoding size 8 (8-byte argument follows) +obj = CBOR_UNSIGNED_INTEGER(0x1234567812345678) +data = bytes(obj) +data == bytes.fromhex('1b1234567812345678') + += uint decoding size 0 +data = bytes.fromhex('12') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == 18 and remainder == b'' + += uint decoding size 1 +data = bytes.fromhex('1834') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == 0x34 and remainder == b'' + += uint decoding size 2 +data = bytes.fromhex('191234') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == 0x1234 and remainder == b'' + += uint decoding size 4 +data = bytes.fromhex('1a12345678') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == 0x12345678 and remainder == b'' + += uint decoding size 8 +data = bytes.fromhex('1b1234567812345678') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == 0x1234567812345678 and remainder == b'' + ++ CBOR Encoding Sizes - Negative Integers + += nint encoding size 0 +obj = CBOR_NEGATIVE_INTEGER(-0x13) +data = bytes(obj) +data == bytes.fromhex('32') + += nint decoding size 0 +data = bytes.fromhex('32') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == -0x13 and isinstance(obj, CBOR_NEGATIVE_INTEGER) and remainder == b'' + += nint decoding size 2 +data = bytes.fromhex('391234') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == (-0x1234 - 1) and isinstance(obj, CBOR_NEGATIVE_INTEGER) and remainder == b'' + ++ CBOR Byte String Edge Cases + += bstr encoding with specific content +obj = CBOR_BYTE_STRING(b'hi') +data = bytes(obj) +data == bytes.fromhex('426869') + += bstr decoding with specific content +data = bytes.fromhex('426869') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == b'hi' and isinstance(obj, CBOR_BYTE_STRING) and remainder == b'' + += bstr longer content (24 bytes) +content = b'longlonglonglonglonglong' +obj = CBOR_BYTE_STRING(content) +data = bytes(obj) +# Should use 1-byte length encoding (0x58 = major type 2, additional info 24) +data[:2] == bytes.fromhex('5818') and data[2:] == content + += bstr decoding longer content +data = bytes.fromhex('58186c6f6e676c6f6e676c6f6e676c6f6e676c6f6e676c6f6e67') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == b'longlonglonglonglonglong' and remainder == b'' + ++ CBOR Text String Edge Cases + += tstr encoding with specific content +obj = CBOR_TEXT_STRING('hi') +data = bytes(obj) +data == bytes.fromhex('626869') + += tstr decoding with specific content +data = bytes.fromhex('626869') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == 'hi' and isinstance(obj, CBOR_TEXT_STRING) and remainder == b'' + += tstr longer content (24 chars) +content = 'longlonglonglonglonglong' +obj = CBOR_TEXT_STRING(content) +data = bytes(obj) +# Should use 1-byte length encoding (0x78 = major type 3, additional info 24) +data[:2] == bytes.fromhex('7818') and data[2:] == content.encode('utf8') + += tstr decoding longer content +data = bytes.fromhex('78186c6f6e676c6f6e676c6f6e676c6f6e676c6f6e676c6f6e67') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +obj.val == 'longlonglonglonglonglong' and remainder == b'' + ++ CBOR Array Specific Encodings + += array encoding with mixed integer types +from scapy.cbor.cborcodec import CBORcodec_ARRAY +# Array with positive 10 and negative 20 +encoded = CBORcodec_ARRAY.enc([10, -20]) +decoded, _ = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_ARRAY) and len(decoded.val) == 2 + += array decoding specific encoding +data = bytes.fromhex('820A33') # array(2): [10, -20] +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 2 and remainder == b'' + ++ CBOR Map Specific Encodings + += map encoding with integer keys +from scapy.cbor.cborcodec import CBORcodec_MAP +encoded = CBORcodec_MAP.enc({10: -20}) +decoded, _ = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_MAP) and len(decoded.val) == 1 + += map decoding specific encoding +data = bytes.fromhex('A10A33') # map(1): {10: -20} +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_MAP) and len(obj.val) == 1 and remainder == b'' + ++ CBOR Float Specific Encodings + += float64 encoding specific value +obj = CBOR_FLOAT(1.5e20) +data = bytes(obj) +data == bytes.fromhex('FB442043561A882930') + += float64 decoding specific value +data = bytes.fromhex('FB442043561A882930') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and obj.val == 1.5e20 and remainder == b'' + ++ CBOR Multiple Item Decoding + += decode multiple items in sequence +data = bytes.fromhex('010203') # Three unsigned integers: 1, 2, 3 +obj1, remainder1 = CBOR_Codecs.CBOR.dec(data) +obj2, remainder2 = CBOR_Codecs.CBOR.dec(remainder1) +obj3, remainder3 = CBOR_Codecs.CBOR.dec(remainder2) +obj1.val == 1 and obj2.val == 2 and obj3.val == 3 and remainder3 == b'' + += decode nested array with specific encoding +data = bytes.fromhex('8201820203') # array(2): [1, array(2): [2, 3]] +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 2 and remainder == b'' and isinstance(obj.val[1], CBOR_ARRAY) + ++ CBOR Boundary Value Tests + += encode maximum value that fits in each size +# Maximum for size 0 (0-23) +obj = CBOR_UNSIGNED_INTEGER(23) +bytes(obj) == bytes.fromhex('17') + += encode minimum value needing size 1 +obj = CBOR_UNSIGNED_INTEGER(24) +bytes(obj) == bytes.fromhex('1818') + += encode maximum value for size 1 +obj = CBOR_UNSIGNED_INTEGER(255) +bytes(obj) == bytes.fromhex('18ff') + += encode minimum value needing size 2 +obj = CBOR_UNSIGNED_INTEGER(256) +bytes(obj) == bytes.fromhex('190100') + += negative integer boundary at -24 +obj = CBOR_NEGATIVE_INTEGER(-24) +bytes(obj) == bytes.fromhex('37') + += negative integer boundary at -25 +obj = CBOR_NEGATIVE_INTEGER(-25) +bytes(obj) == bytes.fromhex('3818') + ++ CBOR Empty Container Tests + += encode empty array +from scapy.cbor.cborcodec import CBORcodec_ARRAY +encoded = CBORcodec_ARRAY.enc([]) +decoded, _ = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_ARRAY) and len(decoded.val) == 0 + += encode empty map +from scapy.cbor.cborcodec import CBORcodec_MAP +encoded = CBORcodec_MAP.enc({}) +decoded, _ = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_MAP) and len(decoded.val) == 0 + += encode empty byte string +obj = CBOR_BYTE_STRING(b'') +data = bytes(obj) +data == bytes.fromhex('40') + += encode empty text string +obj = CBOR_TEXT_STRING('') +data = bytes(obj) +data == bytes.fromhex('60') From 08e2ba68a08dc0373074de18b760d5485ee971c0 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 11 Feb 2026 21:55:03 +0100 Subject: [PATCH 4/6] Remove `advanced_usage.rst` documentation. --- doc/scapy/advanced_usage.rst | 1444 ----------------------------- doc/scapy/advanced_usage/cbor.rst | 218 +++++ 2 files changed, 218 insertions(+), 1444 deletions(-) delete mode 100644 doc/scapy/advanced_usage.rst create mode 100644 doc/scapy/advanced_usage/cbor.rst diff --git a/doc/scapy/advanced_usage.rst b/doc/scapy/advanced_usage.rst deleted file mode 100644 index 6606cb5cdc2..00000000000 --- a/doc/scapy/advanced_usage.rst +++ /dev/null @@ -1,1444 +0,0 @@ -************** -Advanced usage -************** - -ASN.1 and SNMP -============== - -What is ASN.1? --------------- - -.. note:: - - This is only my view on ASN.1, explained as simply as possible. For more theoretical or academic views, I'm sure you'll find better on the Internet. - -ASN.1 is a notation whose goal is to specify formats for data exchange. It is independent of the way data is encoded. Data encoding is specified in Encoding Rules. - -The most used encoding rules are BER (Basic Encoding Rules) and DER (Distinguished Encoding Rules). Both look the same, but the latter is specified to guarantee uniqueness of encoding. This property is quite interesting when speaking about cryptography, hashes, and signatures. - -ASN.1 provides basic objects: integers, many kinds of strings, floats, booleans, containers, etc. They are grouped in the so-called Universal class. A given protocol can provide other objects which will be grouped in the Context class. For example, SNMP defines PDU_GET or PDU_SET objects. There are also the Application and Private classes. - -Each of these objects is given a tag that will be used by the encoding rules. Tags from 1 are used for Universal class. 1 is boolean, 2 is an integer, 3 is a bit string, 6 is an OID, 48 is for a sequence. Tags from the ``Context`` class begin at 0xa0. When encountering an object tagged by 0xa0, we'll need to know the context to be able to decode it. For example, in SNMP context, 0xa0 is a PDU_GET object, while in X509 context, it is a container for the certificate version. - -Other objects are created by assembling all those basic brick objects. The composition is done using sequences and arrays (sets) of previously defined or existing objects. The final object (an X509 certificate, a SNMP packet) is a tree whose non-leaf nodes are sequences and sets objects (or derived context objects), and whose leaf nodes are integers, strings, OID, etc. - -Scapy and ASN.1 ---------------- - -Scapy provides a way to easily encode or decode ASN.1 and also program those encoders/decoders. It is quite laxer than what an ASN.1 parser should be, and it kind of ignores constraints. It won't replace neither an ASN.1 parser nor an ASN.1 compiler. Actually, it has been written to be able to encode and decode broken ASN.1. It can handle corrupted encoded strings and can also create those. - -ASN.1 engine -^^^^^^^^^^^^ - -Note: many of the classes definitions presented here use metaclasses. If you don't look precisely at the source code and you only rely on my captures, you may think they sometimes exhibit a kind of magic behavior. -`` -Scapy ASN.1 engine provides classes to link objects and their tags. They inherit from the ``ASN1_Class``. The first one is ``ASN1_Class_UNIVERSAL``, which provide tags for most Universal objects. Each new context (``SNMP``, ``X509``) will inherit from it and add its own objects. - -:: - - class ASN1_Class_UNIVERSAL(ASN1_Class): - name = "UNIVERSAL" - # [...] - BOOLEAN = 1 - INTEGER = 2 - BIT_STRING = 3 - # [...] - - class ASN1_Class_SNMP(ASN1_Class_UNIVERSAL): - name="SNMP" - PDU_GET = 0xa0 - PDU_NEXT = 0xa1 - PDU_RESPONSE = 0xa2 - - class ASN1_Class_X509(ASN1_Class_UNIVERSAL): - name="X509" - CONT0 = 0xa0 - CONT1 = 0xa1 - # [...] - -All ASN.1 objects are represented by simple Python instances that act as nutshells for the raw values. The simple logic is handled by ``ASN1_Object`` whose they inherit from. Hence they are quite simple:: - - class ASN1_INTEGER(ASN1_Object): - tag = ASN1_Class_UNIVERSAL.INTEGER - - class ASN1_STRING(ASN1_Object): - tag = ASN1_Class_UNIVERSAL.STRING - - class ASN1_BIT_STRING(ASN1_STRING): - tag = ASN1_Class_UNIVERSAL.BIT_STRING - -These instances can be assembled to create an ASN.1 tree:: - - >>> x=ASN1_SEQUENCE([ASN1_INTEGER(7),ASN1_STRING("egg"),ASN1_SEQUENCE([ASN1_BOOLEAN(False)])]) - >>> x - , , ]]>]]> - >>> x.show() - # ASN1_SEQUENCE: - - - # ASN1_SEQUENCE: - - -Encoding engines -^^^^^^^^^^^^^^^^^ - -As with the standard, ASN.1 and encoding are independent. We have just seen how to create a compounded ASN.1 object. To encode or decode it, we need to choose an encoding rule. Scapy provides only BER for the moment (actually, it may be DER. DER looks like BER except only minimal encoding is authorised which may well be what I did). I call this an ASN.1 codec. - -Encoding and decoding are done using class methods provided by the codec. For example the ``BERcodec_INTEGER`` class provides a ``.enc()`` and a ``.dec()`` class methods that can convert between an encoded string and a value of their type. They all inherit from BERcodec_Object which is able to decode objects from any type:: - - >>> BERcodec_INTEGER.enc(7) - '\x02\x01\x07' - >>> BERcodec_BIT_STRING.enc("egg") - '\x03\x03egg' - >>> BERcodec_STRING.enc("egg") - '\x04\x03egg' - >>> BERcodec_STRING.dec('\x04\x03egg') - (, '') - >>> BERcodec_STRING.dec('\x03\x03egg') - Traceback (most recent call last): - File "", line 1, in ? - File "/usr/bin/scapy", line 2099, in dec - return cls.do_dec(s, context, safe) - File "/usr/bin/scapy", line 2178, in do_dec - l,s,t = cls.check_type_check_len(s) - File "/usr/bin/scapy", line 2076, in check_type_check_len - l,s3 = cls.check_type_get_len(s) - File "/usr/bin/scapy", line 2069, in check_type_get_len - s2 = cls.check_type(s) - File "/usr/bin/scapy", line 2065, in check_type - (cls.__name__, ord(s[0]), ord(s[0]),cls.tag), remaining=s) - BER_BadTag_Decoding_Error: BERcodec_STRING: Got tag [3/0x3] while expecting - ### Already decoded ### - None - ### Remaining ### - '\x03\x03egg' - >>> BERcodec_Object.dec('\x03\x03egg') - (, '') - -ASN.1 objects are encoded using their ``.enc()`` method. This method must be called with the codec we want to use. All codecs are referenced in the ASN1_Codecs object. ``raw()`` can also be used. In this case, the default codec (``conf.ASN1_default_codec``) will be used. - -:: - - >>> x.enc(ASN1_Codecs.BER) - '0\r\x02\x01\x07\x04\x03egg0\x03\x01\x01\x00' - >>> raw(x) - '0\r\x02\x01\x07\x04\x03egg0\x03\x01\x01\x00' - >>> xx,remain = BERcodec_Object.dec(_) - >>> xx.show() - # ASN1_SEQUENCE: - - - # ASN1_SEQUENCE: - - - >>> remain - '' - -By default, decoding is done using the ``Universal`` class, which means objects defined in the ``Context`` class will not be decoded. There is a good reason for that: the decoding depends on the context! - -:: - - >>> cert=""" - ... MIIF5jCCA86gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMC - ... VVMxHTAbBgNVBAoTFEFPTCBUaW1lIFdhcm5lciBJbmMuMRwwGgYDVQQLExNB - ... bWVyaWNhIE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIg - ... Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyOTA2MDAw - ... MFoXDTM3MDkyODIzNDMwMFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRB - ... T0wgVGltZSBXYXJuZXIgSW5jLjEcMBoGA1UECxMTQW1lcmljYSBPbmxpbmUg - ... SW5jLjE3MDUGA1UEAxMuQU9MIFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNh - ... dGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC - ... ggIBALQ3WggWmRToVbEbJGv8x4vmh6mJ7ouZzU9AhqS2TcnZsdw8TQ2FTBVs - ... RotSeJ/4I/1n9SQ6aF3Q92RhQVSji6UI0ilbm2BPJoPRYxJWSXakFsKlnUWs - ... i4SVqBax7J/qJBrvuVdcmiQhLE0OcR+mrF1FdAOYxFSMFkpBd4aVdQxHAWZg - ... /BXxD+r1FHjHDtdugRxev17nOirYlxcwfACtCJ0zr7iZYYCLqJV+FNwSbKTQ - ... 2O9ASQI2+W6p1h2WVgSysy0WVoaP2SBXgM1nEG2wTPDaRrbqJS5Gr42whTg0 - ... ixQmgiusrpkLjhTXUr2eacOGAgvqdnUxCc4zGSGFQ+aJLZ8lN2fxI2rSAG2X - ... +Z/nKcrdH9cG6rjJuQkhn8g/BsXS6RJGAE57COtCPStIbp1n3UsC5ETzkxml - ... J85per5n0/xQpCyrw2u544BMzwVhSyvcG7mm0tCq9Stz+86QNZ8MUhy/XCFh - ... EVsVS6kkUfykXPcXnbDS+gfpj1bkGoxoigTTfFrjnqKhynFbotSg5ymFXQNo - ... Kk/SBtc9+cMDLz9l+WceR0DTYw/j1Y75hauXTLPXJuuWCpTehTacyH+BCQJJ - ... Kg71ZDIMgtG6aoIbs0t0EfOMd9afv9w3pKdVBC/UMejTRrkDfNoSTllkt1Ex - ... MVCgyhwn2RAurda9EGYrw7AiShJbAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMB - ... Af8wHQYDVR0OBBYEFE9pbQN+nZ8HGEO8txBO1b+pxCAoMB8GA1UdIwQYMBaA - ... FE9pbQN+nZ8HGEO8txBO1b+pxCAoMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG - ... 9w0BAQUFAAOCAgEAO/Ouyuguh4X7ZVnnrREUpVe8WJ8kEle7+z802u6teio0 - ... cnAxa8cZmIDJgt43d15Ui47y6mdPyXSEkVYJ1eV6moG2gcKtNuTxVBFT8zRF - ... ASbI5Rq8NEQh3q0l/HYWdyGQgJhXnU7q7C+qPBR7V8F+GBRn7iTGvboVsNIY - ... vbdVgaxTwOjdaRITQrcCtQVBynlQboIOcXKTRuidDV29rs4prWPVVRaAMCf/ - ... drr3uNZK49m1+VLQTkCpx+XCMseqdiThawVQ68W/ClTluUI8JPu3B5wwn3la - ... 5uBAUhX0/Kr0VvlEl4ftDmVyXr4m+02kLQgH3thcoNyBM5kYJRF3p+v9WAks - ... mWsbivNSPxpNSGDxoPYzAlOL7SUJuA0t7Zdz7NeWH45gDtoQmy8YJPamTQr5 - ... O8t1wswvziRpyQoijlmn94IM19drNZxDAGrElWe6nEXLuA4399xOAU++CrYD - ... 062KRffaJ00psUjf5BHklka9bAI+1lHIlRcBFanyqqryvy9lG2/QuRqT9Y41 - ... xICHPpQvZuTpqP9BnHAqTyo5GJUefvthATxRCC4oGKQWDzH9OmwjkyB24f0H - ... hdFbP9IcczLd+rn4jM8Ch3qaluTtT4mNU0OrDhPAARW0eTjb/G49nlG2uBOL - ... Z8/5fNkiHfZdxRwBL5joeiQYvITX+txyW/fBOmg= - ... """.decode("base64") - >>> (dcert,remain) = BERcodec_Object.dec(cert) - Traceback (most recent call last): - File "", line 1, in ? - File "/usr/bin/scapy", line 2099, in dec - return cls.do_dec(s, context, safe) - File "/usr/bin/scapy", line 2094, in do_dec - return codec.dec(s,context,safe) - File "/usr/bin/scapy", line 2099, in dec - return cls.do_dec(s, context, safe) - File "/usr/bin/scapy", line 2218, in do_dec - o,s = BERcodec_Object.dec(s, context, safe) - File "/usr/bin/scapy", line 2099, in dec - return cls.do_dec(s, context, safe) - File "/usr/bin/scapy", line 2094, in do_dec - return codec.dec(s,context,safe) - File "/usr/bin/scapy", line 2099, in dec - return cls.do_dec(s, context, safe) - File "/usr/bin/scapy", line 2218, in do_dec - o,s = BERcodec_Object.dec(s, context, safe) - File "/usr/bin/scapy", line 2099, in dec - return cls.do_dec(s, context, safe) - File "/usr/bin/scapy", line 2092, in do_dec - raise BER_Decoding_Error("Unknown prefix [%02x] for [%r]" % (p,t), remaining=s) - BER_Decoding_Error: Unknown prefix [a0] for ['\xa0\x03\x02\x01\x02\x02\x01\x010\r\x06\t*\x86H...'] - ### Already decoded ### - [[]] - ### Remaining ### - '\xa0\x03\x02\x01\x02\x02\x01\x010\r\x06\t*\x86H\x86\xf7\r\x01\x01\x05\x05\x000\x81\x831\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x1d0\x1b\x06\x03U\x04\n\x13\x14AOL Time Warner Inc.1\x1c0\x1a\x06\x03U\x04\x0b\x13\x13America Online Inc.1705\x06\x03U\x04\x03\x13.AOL Time Warner Root Certification Authority 20\x1e\x17\r020529060000Z\x17\r370928234300Z0\x81\x831\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x1d0\x1b\x06\x03U\x04\n\x13\x14AOL Time Warner Inc.1\x1c0\x1a\x06\x03U\x04\x0b\x13\x13America Online Inc.1705\x06\x03U\x04\x03\x13.AOL Time Warner Root Certification Authority 20\x82\x02"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x02\x0f\x000\x82\x02\n\x02\x82\x02\x01\x00\xb47Z\x08\x16\x99\x14\xe8U\xb1\x1b$k\xfc\xc7\x8b\xe6\x87\xa9\x89\xee\x8b\x99\xcdO@\x86\xa4\xb6M\xc9\xd9\xb1\xdc\xd6Q\xc8\x95\x17\x01\x15\xa9\xf2\xaa\xaa\xf2\xbf/e\x1bo\xd0\xb9\x1a\x93\xf5\x8e5\xc4\x80\x87>\x94/f\xe4\xe9\xa8\xffA\x9cp*O*9\x18\x95\x1e~\xfba\x01>> (dcert,remain) = BERcodec_Object.dec(cert, context=ASN1_Class_X509) - >>> dcert.show() - # ASN1_SEQUENCE: - # ASN1_SEQUENCE: - # ASN1_X509_CONT0: - - - # ASN1_SEQUENCE: - - - # ASN1_SEQUENCE: - # ASN1_SET: - # ASN1_SEQUENCE: - - - # ASN1_SET: - # ASN1_SEQUENCE: - - - # ASN1_SET: - # ASN1_SEQUENCE: - - - # ASN1_SET: - # ASN1_SEQUENCE: - - - # ASN1_SEQUENCE: - - - # ASN1_SEQUENCE: - # ASN1_SET: - # ASN1_SEQUENCE: - - - # ASN1_SET: - # ASN1_SEQUENCE: - - - # ASN1_SET: - # ASN1_SEQUENCE: - - - # ASN1_SET: - # ASN1_SEQUENCE: - - - # ASN1_SEQUENCE: - # ASN1_SEQUENCE: - - - - # ASN1_X509_CONT3: - # ASN1_SEQUENCE: - # ASN1_SEQUENCE: - - - - # ASN1_SEQUENCE: - - - # ASN1_SEQUENCE: - - - # ASN1_SEQUENCE: - - - - # ASN1_SEQUENCE: - - - \xd6Q\xc8\x95\x17\x01\x15\xa9\xf2\xaa\xaa\xf2\xbf/e\x1bo\xd0\xb9\x1a\x93\xf5\x8e5\xc4\x80\x87>\x94/f\xe4\xe9\xa8\xffA\x9cp*O*9\x18\x95\x1e~\xfba\x01 - -ASN.1 layers -^^^^^^^^^^^^ - -While this may be nice, it's only an ASN.1 encoder/decoder. Nothing related to Scapy yet. - -ASN.1 fields -~~~~~~~~~~~~ - -Scapy provides ASN.1 fields. They will wrap ASN.1 objects and provide the necessary logic to bind a field name to the value. ASN.1 packets will be described as a tree of ASN.1 fields. Then each field name will be made available as a normal ``Packet`` object, in a flat flavor (ex: to access the version field of a SNMP packet, you don't need to know how many containers wrap it). - -Each ASN.1 field is linked to an ASN.1 object through its tag. - - -ASN.1 packets -~~~~~~~~~~~~~ - -ASN.1 packets inherit from the Packet class. Instead of a ``fields_desc`` list of fields, they define ``ASN1_codec`` and ``ASN1_root`` attributes. The first one is a codec (for example: ``ASN1_Codecs.BER``), the second one is a tree compounded with ASN.1 fields. - -A complete example: SNMP ------------------------- - -SNMP defines new ASN.1 objects. We need to define them:: - - class ASN1_Class_SNMP(ASN1_Class_UNIVERSAL): - name="SNMP" - PDU_GET = 0xa0 - PDU_NEXT = 0xa1 - PDU_RESPONSE = 0xa2 - PDU_SET = 0xa3 - PDU_TRAPv1 = 0xa4 - PDU_BULK = 0xa5 - PDU_INFORM = 0xa6 - PDU_TRAPv2 = 0xa7 - -These objects are PDU, and are in fact new names for a sequence container (this is generally the case for context objects: they are old containers with new names). This means creating the corresponding ASN.1 objects and BER codecs is simplistic:: - - class ASN1_SNMP_PDU_GET(ASN1_SEQUENCE): - tag = ASN1_Class_SNMP.PDU_GET - - class ASN1_SNMP_PDU_NEXT(ASN1_SEQUENCE): - tag = ASN1_Class_SNMP.PDU_NEXT - - # [...] - - class BERcodec_SNMP_PDU_GET(BERcodec_SEQUENCE): - tag = ASN1_Class_SNMP.PDU_GET - - class BERcodec_SNMP_PDU_NEXT(BERcodec_SEQUENCE): - tag = ASN1_Class_SNMP.PDU_NEXT - - # [...] - -Metaclasses provide the magic behind the fact that everything is automatically registered and that ASN.1 objects and BER codecs can find each other. - -The ASN.1 fields are also trivial:: - - class ASN1F_SNMP_PDU_GET(ASN1F_SEQUENCE): - ASN1_tag = ASN1_Class_SNMP.PDU_GET - - class ASN1F_SNMP_PDU_NEXT(ASN1F_SEQUENCE): - ASN1_tag = ASN1_Class_SNMP.PDU_NEXT - - # [...] - -Now, the hard part, the ASN.1 packet:: - - SNMP_error = { 0: "no_error", - 1: "too_big", - # [...] - } - - SNMP_trap_types = { 0: "cold_start", - 1: "warm_start", - # [...] - } - - class SNMPvarbind(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( ASN1F_OID("oid","1.3"), - ASN1F_field("value",ASN1_NULL(0)) - ) - - - class SNMPget(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SNMP_PDU_GET( ASN1F_INTEGER("id",0), - ASN1F_enum_INTEGER("error",0, SNMP_error), - ASN1F_INTEGER("error_index",0), - ASN1F_SEQUENCE_OF("varbindlist", [], SNMPvarbind) - ) - - class SNMPnext(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SNMP_PDU_NEXT( ASN1F_INTEGER("id",0), - ASN1F_enum_INTEGER("error",0, SNMP_error), - ASN1F_INTEGER("error_index",0), - ASN1F_SEQUENCE_OF("varbindlist", [], SNMPvarbind) - ) - # [...] - - class SNMP(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_enum_INTEGER("version", 1, {0:"v1", 1:"v2c", 2:"v2", 3:"v3"}), - ASN1F_STRING("community","public"), - ASN1F_CHOICE("PDU", SNMPget(), - SNMPget, SNMPnext, SNMPresponse, SNMPset, - SNMPtrapv1, SNMPbulk, SNMPinform, SNMPtrapv2) - ) - def answers(self, other): - return ( isinstance(self.PDU, SNMPresponse) and - ( isinstance(other.PDU, SNMPget) or - isinstance(other.PDU, SNMPnext) or - isinstance(other.PDU, SNMPset) ) and - self.PDU.id == other.PDU.id ) - # [...] - bind_layers( UDP, SNMP, sport=161) - bind_layers( UDP, SNMP, dport=161) - -That wasn't that much difficult. If you think that can't be that short to implement SNMP encoding/decoding and that I may have cut too much, just look at the complete source code. - -Now, how to use it? As usual:: - - >>> a=SNMP(version=3, PDU=SNMPget(varbindlist=[SNMPvarbind(oid="1.2.3",value=5), - ... SNMPvarbind(oid="3.2.1",value="hello")])) - >>> a.show() - ###[ SNMP ]### - version= v3 - community= 'public' - \PDU\ - |###[ SNMPget ]### - | id= 0 - | error= no_error - | error_index= 0 - | \varbindlist\ - | |###[ SNMPvarbind ]### - | | oid= '1.2.3' - | | value= 5 - | |###[ SNMPvarbind ]### - | | oid= '3.2.1' - | | value= 'hello' - >>> hexdump(a) - 0000 30 2E 02 01 03 04 06 70 75 62 6C 69 63 A0 21 02 0......public.!. - 0010 01 00 02 01 00 02 01 00 30 16 30 07 06 02 2A 03 ........0.0...*. - 0020 02 01 05 30 0B 06 02 7A 01 04 05 68 65 6C 6C 6F ...0...z...hello - >>> send(IP(dst="1.2.3.4")/UDP()/SNMP()) - . - Sent 1 packets. - >>> SNMP(raw(a)).show() - ###[ SNMP ]### - version= - community= - \PDU\ - |###[ SNMPget ]### - | id= - | error= - | error_index= - | \varbindlist\ - | |###[ SNMPvarbind ]### - | | oid= - | | value= - | |###[ SNMPvarbind ]### - | | oid= - | | value= - - - -Resolving OID from a MIB ------------------------- - -About OID objects -^^^^^^^^^^^^^^^^^ - -OID objects are created with an ``ASN1_OID`` class:: - - >>> o1=ASN1_OID("2.5.29.10") - >>> o2=ASN1_OID("1.2.840.113549.1.1.1") - >>> o1,o2 - (, ) - -Loading a MIB -^^^^^^^^^^^^^ - -Scapy can parse MIB files and become aware of a mapping between an OID and its name:: - - >>> load_mib("mib/*") - >>> o1,o2 - (, ) - -The MIB files I've used are attached to this page. - -Scapy's MIB database -^^^^^^^^^^^^^^^^^^^^ - -All MIB information is stored into the conf.mib object. This object can be used to find the OID of a name - -:: - - >>> conf.mib.sha1_with_rsa_signature - '1.2.840.113549.1.1.5' - -or to resolve an OID:: - - >>> conf.mib._oidname("1.2.3.6.1.4.1.5") - 'enterprises.5' - -It is even possible to graph it:: - - >>> conf.mib._make_graph() - - -CBOR -==== - -What is CBOR? -------------- - -.. note:: - - This section provides a practical introduction to CBOR from Scapy's perspective. For the complete specification, see RFC 8949. - -CBOR (Concise Binary Object Representation) is a data format whose goal is to provide a compact, self-describing binary data interchange format based on the JSON data model. It is defined in RFC 8949 and is designed to be small in code size, reasonably small in message size, and extensible without the need for version negotiation. - -CBOR provides basic data types including: - -* **Unsigned integers** (major type 0): Non-negative integers -* **Negative integers** (major type 1): Negative integers -* **Byte strings** (major type 2): Raw binary data -* **Text strings** (major type 3): UTF-8 encoded strings -* **Arrays** (major type 4): Ordered sequences of values -* **Maps** (major type 5): Unordered key-value pairs -* **Semantic tags** (major type 6): Tagged values with additional semantics -* **Simple values and floats** (major type 7): Booleans, null, undefined, and floating-point numbers - -Each CBOR data item begins with an initial byte that encodes the major type (in the top 3 bits) and additional information (in the low 5 bits). This design allows for compact encoding while maintaining self-describing properties. - -Scapy and CBOR --------------- - -Scapy provides a complete CBOR encoder and decoder following the same architectural pattern as the ASN.1 implementation. The CBOR engine can encode Python objects to CBOR binary format and decode CBOR data back to Python objects. It has been designed to be RFC 8949 compliant and interoperable with other CBOR implementations. - -CBOR engine -^^^^^^^^^^^ - -Scapy's CBOR engine provides classes to represent CBOR data items. The main components are: - -* ``CBOR_MajorTypes``: Defines the 8 major types (0-7) used in CBOR encoding -* ``CBOR_Object``: Base class for all CBOR value objects -* ``CBOR_Codecs``: Registry for encoding/decoding rules - -The ``CBOR_MajorTypes`` class defines tags for all major types:: - - class CBOR_MajorTypes: - name = "CBOR_MAJOR_TYPES" - UNSIGNED_INTEGER = 0 - NEGATIVE_INTEGER = 1 - BYTE_STRING = 2 - TEXT_STRING = 3 - ARRAY = 4 - MAP = 5 - TAG = 6 - SIMPLE_AND_FLOAT = 7 - -All CBOR objects are represented by Python instances that wrap raw values. They inherit from ``CBOR_Object``:: - - class CBOR_UNSIGNED_INTEGER(CBOR_Object): - tag = CBOR_MajorTypes.UNSIGNED_INTEGER - - class CBOR_TEXT_STRING(CBOR_Object): - tag = CBOR_MajorTypes.TEXT_STRING - - class CBOR_ARRAY(CBOR_Object): - tag = CBOR_MajorTypes.ARRAY - -Creating CBOR objects -^^^^^^^^^^^^^^^^^^^^^ - -CBOR objects can be easily created and composed:: - - >>> from scapy.cbor import * - >>> # Create basic types - >>> num = CBOR_UNSIGNED_INTEGER(42) - >>> text = CBOR_TEXT_STRING("Hello, CBOR!") - >>> data = CBOR_BYTE_STRING(b'\x01\x02\x03') - >>> - >>> # Create collections - >>> arr = CBOR_ARRAY([CBOR_UNSIGNED_INTEGER(1), - ... CBOR_UNSIGNED_INTEGER(2), - ... CBOR_TEXT_STRING("three")]) - >>> arr - , , ]]> - >>> - >>> # Create maps - >>> from scapy.cbor.cborcodec import CBORcodec_MAP - >>> mapping = {"name": "Alice", "age": 30, "active": True} - -Encoding and decoding -^^^^^^^^^^^^^^^^^^^^^ - -CBOR objects are encoded using their ``.enc()`` method. All codecs are referenced in the ``CBOR_Codecs`` object. The default codec is ``CBOR_Codecs.CBOR``:: - - >>> num = CBOR_UNSIGNED_INTEGER(42) - >>> encoded = bytes(num) - >>> encoded.hex() - '182a' - >>> - >>> # Decode back - >>> decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) - >>> decoded.val - 42 - >>> isinstance(decoded, CBOR_UNSIGNED_INTEGER) - True - -Encoding collections:: - - >>> from scapy.cbor.cborcodec import CBORcodec_ARRAY, CBORcodec_MAP - >>> # Encode an array - >>> encoded = CBORcodec_ARRAY.enc([1, 2, 3, 4, 5]) - >>> encoded.hex() - '850102030405' - >>> - >>> # Decode the array - >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) - >>> [item.val for item in decoded.val] - [1, 2, 3, 4, 5] - >>> - >>> # Encode a map - >>> encoded = CBORcodec_MAP.enc({"x": 100, "y": 200}) - >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) - >>> isinstance(decoded, CBOR_MAP) - True - -Working with different types -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -CBOR supports various data types:: - - >>> # Booleans - >>> true_val = CBOR_TRUE() - >>> false_val = CBOR_FALSE() - >>> bytes(true_val).hex() - 'f5' - >>> bytes(false_val).hex() - 'f4' - >>> - >>> # Null and undefined - >>> null_val = CBOR_NULL() - >>> undef_val = CBOR_UNDEFINED() - >>> bytes(null_val).hex() - 'f6' - >>> bytes(undef_val).hex() - 'f7' - >>> - >>> # Floating point - >>> float_val = CBOR_FLOAT(3.14159) - >>> bytes(float_val).hex() - 'fb400921f9f01b866e' - >>> - >>> # Negative integers - >>> neg = CBOR_NEGATIVE_INTEGER(-100) - >>> bytes(neg).hex() - '3863' - -Complex structures -^^^^^^^^^^^^^^^^^^ - -CBOR supports nested structures:: - - >>> # Nested arrays - >>> nested = CBORcodec_ARRAY.enc([1, [2, 3], [4, [5, 6]]]) - >>> decoded, _ = CBOR_Codecs.CBOR.dec(nested) - >>> isinstance(decoded, CBOR_ARRAY) - True - >>> - >>> # Complex maps with mixed types - >>> data = { - ... "name": "Bob", - ... "age": 25, - ... "active": True, - ... "tags": ["user", "admin"] - ... } - >>> encoded = CBORcodec_MAP.enc(data) - >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) - >>> len(decoded.val) - 4 - -Semantic tags -^^^^^^^^^^^^^ - -CBOR supports semantic tags (major type 6) for providing additional meaning to data items:: - - >>> # Tag 1 is for Unix epoch timestamps - >>> import time - >>> timestamp = int(time.time()) - >>> tagged = CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(timestamp))) - >>> encoded = bytes(tagged) - >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) - >>> decoded.val[0] # Tag number - 1 - -Interoperability -^^^^^^^^^^^^^^^^ - -Scapy's CBOR implementation is fully interoperable with other CBOR libraries. The implementation has been tested with the ``cbor2`` Python library to ensure RFC 8949 compliance:: - - >>> import cbor2 - >>> # Encode with Scapy, decode with cbor2 - >>> scapy_obj = CBOR_UNSIGNED_INTEGER(12345) - >>> scapy_encoded = bytes(scapy_obj) - >>> cbor2.loads(scapy_encoded) - 12345 - >>> - >>> # Encode with cbor2, decode with Scapy - >>> cbor2_encoded = cbor2.dumps([1, "test", True]) - >>> scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) - >>> isinstance(scapy_decoded, CBOR_ARRAY) - True - -Error handling -^^^^^^^^^^^^^^ - -Scapy provides safe decoding with error handling:: - - >>> # Safe decoding returns error objects for invalid data - >>> invalid_data = b'\xff\xff\xff' - >>> obj, remainder = CBOR_Codecs.CBOR.safedec(invalid_data) - >>> isinstance(obj, CBOR_DECODING_ERROR) - True - -Comparison with ASN.1 -^^^^^^^^^^^^^^^^^^^^^ - -While both ASN.1 and CBOR are data serialization formats, they serve different purposes: - -* **ASN.1** is designed for complex schemas with strict typing and multiple encoding rules (BER, DER, PER, etc.) -* **CBOR** is designed for simplicity and compactness, with a single self-describing encoding - -Scapy implements both following the same architectural pattern, making it easy to work with either format. CBOR's simpler structure makes it ideal for IoT, embedded systems, and web APIs, while ASN.1 is prevalent in telecommunications and cryptographic standards. - - -Automata -======== - -Scapy enables to create easily network automata. Scapy does not stick to a specific model like Moore or Mealy automata. It provides a flexible way for you to choose your way to go. - -An automaton in Scapy is deterministic. It has different states. A start state and some end and error states. There are transitions from one state to another. Transitions can be transitions on a specific condition, transitions on the reception of a specific packet or transitions on a timeout. When a transition is taken, one or more actions can be run. An action can be bound to many transitions. Parameters can be passed from states to transitions, and from transitions to states and actions. - -From a programmer's point of view, states, transitions and actions are methods from an Automaton subclass. They are decorated to provide meta-information needed in order for the automaton to work. - -First example -------------- - -Let's begin with a simple example. I take the convention to write states with capitals, but anything valid with Python syntax would work as well. - -:: - - class HelloWorld(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - print("State=BEGIN") - - @ATMT.condition(BEGIN) - def wait_for_nothing(self): - print("Wait for nothing...") - raise self.END() - - @ATMT.action(wait_for_nothing) - def on_nothing(self): - print("Action on 'nothing' condition") - - @ATMT.state(final=1) - def END(self): - print("State=END") - -In this example, we can see 3 decorators: - -* ``ATMT.state`` that is used to indicate that a method is a state, and that can - have initial, final, stop and error optional arguments set to non-zero for special states. -* ``ATMT.condition`` that indicate a method to be run when the automaton state - reaches the indicated state. The argument is the name of the method representing that state -* ``ATMT.action`` binds a method to a transition and is run when the transition is taken. - -Running this example gives the following result:: - - >>> a=HelloWorld() - >>> a.run() - State=BEGIN - Wait for nothing... - Action on 'nothing' condition - State=END - >>> a.destroy() - -This simple automaton can be described with the following graph: - -.. image:: graphics/ATMT_HelloWorld.* - -The graph can be automatically drawn from the code with:: - - >>> HelloWorld.graph() - -.. note:: An ``Automaton`` can be reset using ``restart()``. It is then possible to run it again. - -.. warning:: Remember to call ``destroy()`` once you're done using an Automaton. (especially on PyPy) - -Changing states ---------------- - -The ``ATMT.state`` decorator transforms a method into a function that returns an exception. If you raise that exception, the automaton state will be changed. If the change occurs in a transition, actions bound to this transition will be called. The parameters given to the function replacing the method will be kept and finally delivered to the method. The exception has a method action_parameters that can be called before it is raised so that it will store parameters to be delivered to all actions bound to the current transition. - -As an example, let's consider the following state:: - - @ATMT.state() - def MY_STATE(self, param1, param2): - print("state=MY_STATE. param1=%r param2=%r" % (param1, param2)) - -This state will be reached with the following code:: - - @ATMT.receive_condition(ANOTHER_STATE) - def received_ICMP(self, pkt): - if ICMP in pkt: - raise self.MY_STATE("got icmp", pkt[ICMP].type) - -Let's suppose we want to bind an action to this transition, that will also need some parameters:: - - @ATMT.action(received_ICMP) - def on_ICMP(self, icmp_type, icmp_code): - self.retaliate(icmp_type, icmp_code) - -The condition should become:: - - @ATMT.receive_condition(ANOTHER_STATE) - def received_ICMP(self, pkt): - if ICMP in pkt: - raise self.MY_STATE("got icmp", pkt[ICMP].type).action_parameters(pkt[ICMP].type, pkt[ICMP].code) - -Real example ------------- - -Here is a real example take from Scapy. It implements a TFTP client that can issue read requests. - -.. image:: graphics/ATMT_TFTP_read.* - -:: - - class TFTP_read(Automaton): - def parse_args(self, filename, server, sport = None, port=69, **kargs): - Automaton.parse_args(self, **kargs) - self.filename = filename - self.server = server - self.port = port - self.sport = sport - - def master_filter(self, pkt): - return ( IP in pkt and pkt[IP].src == self.server and UDP in pkt - and pkt[UDP].dport == self.my_tid - and (self.server_tid is None or pkt[UDP].sport == self.server_tid) ) - - # BEGIN - @ATMT.state(initial=1) - def BEGIN(self): - self.blocksize=512 - self.my_tid = self.sport or RandShort()._fix() - bind_bottom_up(UDP, TFTP, dport=self.my_tid) - self.server_tid = None - self.res = b"" - - self.l3 = IP(dst=self.server)/UDP(sport=self.my_tid, dport=self.port)/TFTP() - self.last_packet = self.l3/TFTP_RRQ(filename=self.filename, mode="octet") - self.send(self.last_packet) - self.awaiting=1 - - raise self.WAITING() - - # WAITING - @ATMT.state() - def WAITING(self): - pass - - @ATMT.receive_condition(WAITING) - def receive_data(self, pkt): - if TFTP_DATA in pkt and pkt[TFTP_DATA].block == self.awaiting: - if self.server_tid is None: - self.server_tid = pkt[UDP].sport - self.l3[UDP].dport = self.server_tid - raise self.RECEIVING(pkt) - @ATMT.action(receive_data) - def send_ack(self): - self.last_packet = self.l3 / TFTP_ACK(block = self.awaiting) - self.send(self.last_packet) - - @ATMT.receive_condition(WAITING, prio=1) - def receive_error(self, pkt): - if TFTP_ERROR in pkt: - raise self.ERROR(pkt) - - @ATMT.timeout(WAITING, 3) - def timeout_waiting(self): - raise self.WAITING() - @ATMT.action(timeout_waiting) - def retransmit_last_packet(self): - self.send(self.last_packet) - - # RECEIVED - @ATMT.state() - def RECEIVING(self, pkt): - recvd = pkt[Raw].load - self.res += recvd - self.awaiting += 1 - if len(recvd) == self.blocksize: - raise self.WAITING() - raise self.END() - - # ERROR - @ATMT.state(error=1) - def ERROR(self,pkt): - split_bottom_up(UDP, TFTP, dport=self.my_tid) - return pkt[TFTP_ERROR].summary() - - #END - @ATMT.state(final=1) - def END(self): - split_bottom_up(UDP, TFTP, dport=self.my_tid) - return self.res - -It can be run like this, for instance:: - - >>> atmt = TFTP_read("my_file", "192.168.1.128") - >>> atmt.run() - >>> atmt.destroy() - -Detailed documentation ----------------------- - -Decorators -^^^^^^^^^^ -Decorator for states -~~~~~~~~~~~~~~~~~~~~ - -States are methods decorated by the result of the ``ATMT.state`` function. It can take 4 optional parameters, ``initial``, ``final``, ``stop`` and ``error``, that, when set to ``True``, indicating that the state is an initial, final, stop or error state. - -.. note:: The ``initial`` state is called while starting the automata. The ``final`` step will tell the automata has reached its end. If you call ``atmt.stop()``, the automata will move to the ``stop`` step whatever its current state is. The ``error`` state will mark the automata as errored. If no ``stop`` state is specified, calling ``stop`` and ``forcestop`` will be equivalent. - -:: - - class Example(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - pass - - @ATMT.state() - def SOME_STATE(self): - pass - - @ATMT.state(final=1) - def END(self): - return "Result of the automaton: 42" - - @ATMT.state(stop=1) - def STOP(self): - print("SHUTTING DOWN...") - # e.g. close sockets... - - @ATMT.condition(STOP) - def is_stopping(self): - raise self.END() - - @ATMT.state(error=1) - def ERROR(self): - return "Partial result, or explanation" - # [...] - -Take for instance the TCP client: - -.. image:: graphics/ATMT_TCP_client.svg - -The ``START`` event is ``initial=1``, the ``STOP`` event is ``stop=1`` and the ``CLOSED`` event is ``final=1``. - -Decorators for transitions -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Transitions are methods decorated by the result of one of ``ATMT.condition``, ``ATMT.receive_condition``, ``ATMT.eof``, ``ATMT.timeout``, ``ATMT.timer``. They all take as argument the state method they are related to. ``ATMT.timeout`` and ``ATMT.timer`` also have a mandatory ``timeout`` parameter to provide the timeout value in seconds. The difference between ``ATMT.timeout`` and ``ATMT.timer`` is that ``ATMT.timeout`` gets triggered only once. ``ATMT.timer`` get reloaded automatically, which is useful for sending keep-alive packets. ``ATMT.condition`` and ``ATMT.receive_condition`` have an optional ``prio`` parameter so that the order in which conditions are evaluated can be forced. The default priority is 0. Transitions with the same priority level are called in an undetermined order. - -When the automaton switches to a given state, the state's method is executed. Then transitions methods are called at specific moments until one triggers a new state (something like ``raise self.MY_NEW_STATE()``). First, right after the state's method returns, the ``ATMT.condition`` decorated methods are run by growing prio. Then each time a packet is received and accepted by the master filter all ``ATMT.receive_condition`` decorated hods are called by growing prio. When a timeout is reached since the time we entered into the current space, the corresponding ``ATMT.timeout`` decorated method is called. If the socket raises an ``EOFError`` (closed) during a state, the ``ATMT.EOF`` transition is called. Otherwise it raises an exception and the automaton exits. - -:: - - class Example(Automaton): - @ATMT.state() - def WAITING(self): - pass - - @ATMT.condition(WAITING) - def it_is_raining(self): - if not self.have_umbrella: - raise self.ERROR_WET() - - @ATMT.receive_condition(WAITING, prio=1) - def it_is_ICMP(self, pkt): - if ICMP in pkt: - raise self.RECEIVED_ICMP(pkt) - - @ATMT.receive_condition(WAITING, prio=2) - def it_is_IP(self, pkt): - if IP in pkt: - raise self.RECEIVED_IP(pkt) - - @ATMT.timeout(WAITING, 10.0) - def waiting_timeout(self): - raise self.ERROR_TIMEOUT() - -Decorator for actions -~~~~~~~~~~~~~~~~~~~~~ - -Actions are methods that are decorated by the return of ``ATMT.action`` function. This function takes the transition method it is bound to as first parameter and an optional priority ``prio`` as a second parameter. The default priority is 0. An action method can be decorated many times to be bound to many transitions. - -:: - - from random import random - - class Example(Automaton): - @ATMT.state(initial=1) - def BEGIN(self): - pass - - @ATMT.state(final=1) - def END(self): - pass - - @ATMT.condition(BEGIN, prio=1) - def maybe_go_to_end(self): - if random() > 0.5: - raise self.END() - - @ATMT.condition(BEGIN, prio=2) - def certainly_go_to_end(self): - raise self.END() - - @ATMT.action(maybe_go_to_end) - def maybe_action(self): - print("We are lucky...") - - @ATMT.action(certainly_go_to_end) - def certainly_action(self): - print("We are not lucky...") - - @ATMT.action(maybe_go_to_end, prio=1) - @ATMT.action(certainly_go_to_end, prio=1) - def always_action(self): - print("This wasn't luck!...") - -The two possible outputs are:: - - >>> a=Example() - >>> a.run() - We are not lucky... - This wasn't luck!... - >>> a.run() - We are lucky... - This wasn't luck!... - >>> a.destroy() - - -.. note:: If you want to pass a parameter to an action, you can use the ``action_parameters`` function while raising the next state. - -In the following example, the ``send_copy`` action takes a parameter passed by ``is_fin``:: - - class Example(Automaton): - @ATMT.state() - def WAITING(self): - pass - - @ATMT.state() - def FIN_RECEIVED(self): - pass - - @ATMT.receive_condition(WAITING) - def is_fin(self, pkt): - if pkt[TCP].flags.F: - raise self.FIN_RECEIVED().action_parameters(pkt) - - @ATMT.action(is_fin) - def send_copy(self, pkt): - send(pkt) - - -Methods to overload -^^^^^^^^^^^^^^^^^^^ - -Two methods are hooks to be overloaded: - -* The ``parse_args()`` method is called with arguments given at ``__init__()`` and ``run()``. Use that to parametrize the behavior of your automaton. - -* The ``master_filter()`` method is called each time a packet is sniffed and decides if it is interesting for the automaton. When working on a specific protocol, this is where you will ensure the packet belongs to the connection you are being part of, so that you do not need to make all the sanity checks in each transition. - -Timer configuration -^^^^^^^^^^^^^^^^^^^ - -Some protocols allow timer configuration. In order to configure timeout values during class initialization one may use ``timer_by_name()`` method, which returns ``Timer`` object associated with the given function name:: - - class Example(Automaton): - def __init__(self, *args, **kwargs): - super(Example, self).__init__(*args, **kwargs) - timer = self.timer_by_name("waiting_timeout") - timer.set(1) - - @ATMT.state(initial=1) - def WAITING(self): - pass - - @ATMT.state(final=1) - def END(self): - pass - - @ATMT.timeout(WAITING, 10.0) - def waiting_timeout(self): - raise self.END() - -.. _pipetools: - -PipeTools -========= - -Scapy's ``pipetool`` is a smart piping system allowing to perform complex stream data management. - -The goal is to create a sequence of steps with one or several inputs and one or several outputs, with a bunch of blocks in between. -PipeTools can handle varied sources of data (and outputs) such as user input, pcap input, sniffing, wireshark... -A pipe system is implemented by manually linking all its parts. It is possible to dynamically add an element while running or set multiple drains for the same source. - -.. note:: Pipetool default objects are located inside ``scapy.pipetool`` - -Demo: sniff, anonymize, send to Wireshark ------------------------------------------ - -The following code will sniff packets on the default interface, anonymize the source and destination IP addresses and pipe it all into Wireshark. Useful when posting online examples, for instance. - -.. code-block:: python3 - - source = SniffSource(iface=conf.iface) - wire = WiresharkSink() - def transf(pkt): - if not pkt or IP not in pkt: - return pkt - pkt[IP].src = "1.1.1.1" - pkt[IP].dst = "2.2.2.2" - return pkt - - source > TransformDrain(transf) > wire - p = PipeEngine(source) - p.start() - p.wait_and_stop() - -The engine is pretty straightforward: - -.. image:: graphics/pipetool_demo.svg - -Let's run it: - -.. image:: graphics/animations/pipetool_demo.gif - -Class Types ------------ - -There are 3 different class of objects used for data management: - -- ``Sources`` -- ``Drains`` -- ``Sinks`` - -They are executed and handled by a :class:`~scapy.pipetool.PipeEngine` object. - -When running, a pipetool engine waits for any available data from the Source, and send it in the Drains linked to it. -The data then goes from Drains to Drains until it arrives in a Sink, the final state of this data. - -Let's see with a basic demo how to build a pipetool system. - -.. image:: graphics/pipetool_engine.png - -For instance, this engine was generated with this code: - -.. code:: pycon - - >>> s = CLIFeeder() - >>> s2 = CLIHighFeeder() - >>> d1 = Drain() - >>> d2 = TransformDrain(lambda x: x[::-1]) - >>> si1 = ConsoleSink() - >>> si2 = QueueSink() - >>> - >>> s > d1 - >>> d1 > si1 - >>> d1 > si2 - >>> - >>> s2 >> d1 - >>> d1 >> d2 - >>> d2 >> si1 - >>> - >>> p = PipeEngine() - >>> p.add(s) - >>> p.add(s2) - >>> p.graph(target="> the_above_image.png") - -``start()`` is used to start the :class:`~scapy.pipetool.PipeEngine`: - -.. code:: pycon - - >>> p.start() - -Now, let's play with it by sending some input data - -.. code:: pycon - - >>> s.send("foo") - >'foo' - >>> s2.send("bar") - >>'rab' - >>> s.send("i like potato") - >'i like potato' - >>> print(si2.recv(), ":", si2.recv()) - foo : i like potato - -Let's study what happens here: - -- there are **two canals** in a :class:`~scapy.pipetool.PipeEngine`, a lower one and a higher one. Some Sources write on the lower one, some on the higher one and some on both. -- most sources can be linked to any drain, on both lower and higher canals. The use of ``>`` indicates a link on the low canal, and ``>>`` on the higher one. -- when we send some data in ``s``, which is on the lower canal, as shown above, it goes through the :class:`~scapy.pipetool.Drain` then is sent to the :class:`~.scapy.pipetool.QueueSink` and to the :class:`~scapy.pipetool.ConsoleSink` -- when we send some data in ``s2``, it goes through the Drain, then the TransformDrain where the data is reversed (see the lambda), before being sent to :class:`~scapy.pipetool.ConsoleSink` only. This explains why we only have the data of the lower sources inside the QueueSink: the higher one has not been linked. - -Most of the sinks receive from both lower and upper canals. This is verifiable using the `help(ConsoleSink)` - -.. code:: pycon - - >>> help(ConsoleSink) - Help on class ConsoleSink in module scapy.pipetool: - class ConsoleSink(Sink) - | Print messages on low and high entries - | +-------+ - | >>-|--. |->> - | | print | - | >-|--' |-> - | +-------+ - | - [...] - - -Sources -^^^^^^^ - -A Source is a class that generates some data. - -There are several source types integrated with Scapy, usable as-is, but you may -also create yours. - -Default Source classes -~~~~~~~~~~~~~~~~~~~~~~ - -For any of those class, have a look at ``help([theclass])`` to get more information or the required parameters. - -- :class:`~scapy.pipetool.CLIFeeder` : a source especially used in interactive software. its ``send(data)`` generates the event data on the lower canal -- :class:`~scapy.pipetool.CLIHighFeeder` : same than CLIFeeder, but writes on the higher canal -- :class:`~scapy.pipetool.PeriodicSource` : Generate messages periodically on the low canal. -- :class:`~scapy.pipetool.AutoSource`: the default source, that must be extended to create custom sources. - -Create a custom Source -~~~~~~~~~~~~~~~~~~~~~~ - -To create a custom source, one must extend the :class:`~scapy.pipetool.AutoSource` class. - -.. note:: - - Do NOT use the default :class:`~scapy.pipetool.Source` class except if you are really sure of what you are doing: it is only used internally, and is missing some implementation. The :class:`~scapy.pipetool.AutoSource` is made to be used. - - -To send data through it, the object must call its ``self._gen_data(msg)`` or ``self._gen_high_data(msg)`` functions, which send the data into the PipeEngine. - -The Source should also (if possible), set ``self.is_exhausted`` to ``True`` when empty, to allow the clean stop of the :class:`~scapy.pipetool.PipeEngine`. If the source is infinite, it will need a force-stop (see PipeEngine below) - -For instance, here is how :class:`~scapy.pipetool.CLIHighFeeder` is implemented: - -.. code:: python3 - - class CLIFeeder(CLIFeeder): - def send(self, msg): - self._gen_high_data(msg) - def close(self): - self.is_exhausted = True - -Drains -^^^^^^ - -Default Drain classes -~~~~~~~~~~~~~~~~~~~~~ - -Drains need to be linked on the entry that you are using. It can be either on the lower one (using ``>``) or the upper one (using ``>>``). -See the basic example above. - -- :class:`~scapy.pipetool.Drain` : the most basic Drain possible. Will pass on both low and high entry if linked properly. -- :class:`~scapy.pipetool.TransformDrain` : Apply a function to messages on low and high entry -- :class:`~scapy.pipetool.UpDrain` : Repeat messages from low entry to high exit -- :class:`~scapy.pipetool.DownDrain` : Repeat messages from high entry to low exit - -Create a custom Drain -~~~~~~~~~~~~~~~~~~~~~ - -To create a custom drain, one must extend the :class:`~scapy.pipetool.Drain` class. - -A :class:`~scapy.pipetool.Drain` object will receive data from the lower canal in its ``push`` method, and from the higher canal from its ``high_push`` method. - -To send the data back into the next linked Drain / Sink, it must call the ``self._send(msg)`` or ``self._high_send(msg)`` methods. - -For instance, here is how :class:`~scapy.pipetool.TransformDrain` is implemented:: - - class TransformDrain(Drain): - def __init__(self, f, name=None): - Drain.__init__(self, name=name) - self.f = f - def push(self, msg): - self._send(self.f(msg)) - def high_push(self, msg): - self._high_send(self.f(msg)) - -Sinks -^^^^^ - -Sinks are destinations for messages. - -A :py:class:`~scapy.pipetool.Sink` receives data like a :py:class:`~scapy.pipetool.Drain`, but doesn't send any -messages after it. - -Messages on the low entry come from :py:meth:`~scapy.pipetool.Sink.push`, and messages on the -high entry come from :py:meth:`~scapy.pipetool.Sink.high_push`. - -Default Sinks classes -~~~~~~~~~~~~~~~~~~~~~ - -- :class:`~scapy.pipetool.ConsoleSink` : Print messages on low and high entries to ``stdout`` -- :class:`~scapy.pipetool.RawConsoleSink` : Print messages on low and high entries, using os.write -- :class:`~scapy.pipetool.TermSink` : Prints messages on the low and high entries, on a separate terminal -- :class:`~scapy.pipetool.QueueSink` : Collects messages on the low and high entries into a :py:class:`Queue` - -Create a custom Sink -~~~~~~~~~~~~~~~~~~~~ - -To create a custom sink, one must extend :py:class:`~scapy.pipetool.Sink` and implement -:py:meth:`~scapy.pipetool.Sink.push` and/or :py:meth:`~scapy.pipetool.Sink.high_push`. - -This is a simplified version of :py:class:`~scapy.pipetool.ConsoleSink`: - -.. code-block:: python3 - - class ConsoleSink(Sink): - def push(self, msg): - print(">%r" % msg) - def high_push(self, msg): - print(">>%r" % msg) - -Link objects ------------- - -As shown in the example, most sources can be linked to any drain, on both low -and high entry. - -The use of ``>`` indicates a link on the low entry, and ``>>`` on the high -entry. - -For example, to link ``a``, ``b`` and ``c`` on the low entries: - -.. code-block:: pycon - - >>> a = CLIFeeder() - >>> b = Drain() - >>> c = ConsoleSink() - >>> a > b > c - >>> p = PipeEngine() - >>> p.add(a) - -This wouldn't link the high entries, so something like this would do nothing: - -.. code-block:: pycon - - >>> a2 = CLIHighFeeder() - >>> a2 >> b - >>> a2.send("hello") - -Because ``b`` (:py:class:`~scapy.pipetool.Drain`) and ``c`` (:py:class:`scapy.pipetool.ConsoleSink`) are not -linked on the high entry. - -However, using a :py:class:`~scapy.pipetool.DownDrain` would bring the high messages from -:py:class:`~scapy.pipetool.CLIHighFeeder` to the lower channel: - -.. code-block:: pycon - - >>> a2 = CLIHighFeeder() - >>> b2 = DownDrain() - >>> a2 >> b2 - >>> b2 > b - >>> a2.send("hello") - -The PipeEngine class --------------------- - -The :class:`~scapy.pipetool.PipeEngine` class is the core class of the Pipetool system. It must be initialized and passed the list of all Sources. - -There are two ways of passing sources: - -- during initialization: ``p = PipeEngine(source1, source2, ...)`` -- using the ``add(source)`` method - -A :class:`~scapy.pipetool.PipeEngine` class must be started with ``.start()`` function. It may be force-stopped with the ``.stop()``, or cleanly stopped with ``.wait_and_stop()`` - -A clean stop only works if the Sources is exhausted (has no data to send left). - -It can be printed into a graph using ``.graph()`` methods. see ``help(do_graph)`` for the list of available keyword arguments. - -Scapy advanced PipeTool objects -------------------------------- - -.. note:: Unlike the previous objects, those are not located in ``scapy.pipetool`` but in ``scapy.scapypipes`` - -Now that you know the default PipeTool objects, here are some more advanced ones, based on packet functionalities. - -- :class:`~scapy.scapypipes.SniffSource` : Read packets from an interface and send them to low exit. -- :class:`~scapy.scapypipes.RdpcapSource` : Read packets from a PCAP file send them to low exit. -- :class:`~scapy.scapypipes.InjectSink` : Packets received on low input are injected (sent) to an interface -- :class:`~scapy.scapypipes.WrpcapSink` : Packets received on low input are written to PCAP file -- :class:`~scapy.scapypipes.UDPDrain` : UDP payloads received on high entry are sent over UDP (complicated, have a look at ``help(UDPDrain)``) -- :class:`~scapy.scapypipes.FDSourceSink` : Use a file descriptor as source and sink -- :class:`~scapy.scapypipes.TCPConnectPipe`: TCP connect to addr:port and use it as source and sink -- :class:`~scapy.scapypipes.TCPListenPipe` : TCP listen on [addr:]port and use the first connection as source and sink (complicated, have a look at ``help(TCPListenPipe)``) - -Triggering ----------- - -Some special sort of Drains exists: the Trigger Drains. - -Trigger Drains are special drains, that on receiving data not only pass it by but also send a "Trigger" input, that is received and handled by the next triggered drain (if it exists). - -For example, here is a basic :class:`~scapy.scapypipes.TriggerDrain` usage: - -.. code:: pycon - - >>> a = CLIFeeder() - >>> d = TriggerDrain(lambda msg: True) # Pass messages and trigger when a condition is met - >>> d2 = TriggeredValve() - >>> s = ConsoleSink() - >>> a > d > d2 > s - >>> d ^ d2 # Link the triggers - >>> p = PipeEngine(s) - >>> p.start() - INFO: Pipe engine thread started. - >>> - >>> a.send("this will be printed") - >'this will be printed' - >>> a.send("this won't, because the valve was switched") - >>> a.send("this will, because the valve was switched again") - >'this will, because the valve was switched again' - >>> p.stop() - -Several triggering Drains exist, they are pretty explicit. It is highly recommended to check the doc using ``help([the class])`` - -- :class:`~scapy.scapypipes.TriggeredMessage` : Send a preloaded message when triggered and trigger in chain -- :class:`~scapy.scapypipes.TriggerDrain` : Pass messages and trigger when a condition is met -- :class:`~scapy.scapypipes.TriggeredValve` : Let messages alternatively pass or not, changing on trigger -- :class:`~scapy.scapypipes.TriggeredQueueingValve` : Let messages alternatively pass or queued, changing on trigger -- :class:`~scapy.scapypipes.TriggeredSwitch` : Let messages alternatively high or low, changing on trigger diff --git a/doc/scapy/advanced_usage/cbor.rst b/doc/scapy/advanced_usage/cbor.rst new file mode 100644 index 00000000000..2558ad62f6e --- /dev/null +++ b/doc/scapy/advanced_usage/cbor.rst @@ -0,0 +1,218 @@ +CBOR +==== + +What is CBOR? +------------- + +.. note:: + + This section provides a practical introduction to CBOR from Scapy's perspective. For the complete specification, see RFC 8949. + +CBOR (Concise Binary Object Representation) is a data format whose goal is to provide a compact, self-describing binary data interchange format based on the JSON data model. It is defined in RFC 8949 and is designed to be small in code size, reasonably small in message size, and extensible without the need for version negotiation. + +CBOR provides basic data types including: + +* **Unsigned integers** (major type 0): Non-negative integers +* **Negative integers** (major type 1): Negative integers +* **Byte strings** (major type 2): Raw binary data +* **Text strings** (major type 3): UTF-8 encoded strings +* **Arrays** (major type 4): Ordered sequences of values +* **Maps** (major type 5): Unordered key-value pairs +* **Semantic tags** (major type 6): Tagged values with additional semantics +* **Simple values and floats** (major type 7): Booleans, null, undefined, and floating-point numbers + +Each CBOR data item begins with an initial byte that encodes the major type (in the top 3 bits) and additional information (in the low 5 bits). This design allows for compact encoding while maintaining self-describing properties. + +Scapy and CBOR +-------------- + +Scapy provides a complete CBOR encoder and decoder following the same architectural pattern as the ASN.1 implementation. The CBOR engine can encode Python objects to CBOR binary format and decode CBOR data back to Python objects. It has been designed to be RFC 8949 compliant and interoperable with other CBOR implementations. + +CBOR engine +^^^^^^^^^^^ + +Scapy's CBOR engine provides classes to represent CBOR data items. The main components are: + +* ``CBOR_MajorTypes``: Defines the 8 major types (0-7) used in CBOR encoding +* ``CBOR_Object``: Base class for all CBOR value objects +* ``CBOR_Codecs``: Registry for encoding/decoding rules + +The ``CBOR_MajorTypes`` class defines tags for all major types:: + + class CBOR_MajorTypes: + name = "CBOR_MAJOR_TYPES" + UNSIGNED_INTEGER = 0 + NEGATIVE_INTEGER = 1 + BYTE_STRING = 2 + TEXT_STRING = 3 + ARRAY = 4 + MAP = 5 + TAG = 6 + SIMPLE_AND_FLOAT = 7 + +All CBOR objects are represented by Python instances that wrap raw values. They inherit from ``CBOR_Object``:: + + class CBOR_UNSIGNED_INTEGER(CBOR_Object): + tag = CBOR_MajorTypes.UNSIGNED_INTEGER + + class CBOR_TEXT_STRING(CBOR_Object): + tag = CBOR_MajorTypes.TEXT_STRING + + class CBOR_ARRAY(CBOR_Object): + tag = CBOR_MajorTypes.ARRAY + +Creating CBOR objects +^^^^^^^^^^^^^^^^^^^^^ + +CBOR objects can be easily created and composed:: + + >>> from scapy.cbor import * + >>> # Create basic types + >>> num = CBOR_UNSIGNED_INTEGER(42) + >>> text = CBOR_TEXT_STRING("Hello, CBOR!") + >>> data = CBOR_BYTE_STRING(b'\x01\x02\x03') + >>> + >>> # Create collections + >>> arr = CBOR_ARRAY([CBOR_UNSIGNED_INTEGER(1), + ... CBOR_UNSIGNED_INTEGER(2), + ... CBOR_TEXT_STRING("three")]) + >>> arr + , , ]]> + >>> + >>> # Create maps + >>> from scapy.cbor.cborcodec import CBORcodec_MAP + >>> mapping = {"name": "Alice", "age": 30, "active": True} + +Encoding and decoding +^^^^^^^^^^^^^^^^^^^^^ + +CBOR objects are encoded using their ``.enc()`` method. All codecs are referenced in the ``CBOR_Codecs`` object. The default codec is ``CBOR_Codecs.CBOR``:: + + >>> num = CBOR_UNSIGNED_INTEGER(42) + >>> encoded = bytes(num) + >>> encoded.hex() + '182a' + >>> + >>> # Decode back + >>> decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) + >>> decoded.val + 42 + >>> isinstance(decoded, CBOR_UNSIGNED_INTEGER) + True + +Encoding collections:: + + >>> from scapy.cbor.cborcodec import CBORcodec_ARRAY, CBORcodec_MAP + >>> # Encode an array + >>> encoded = CBORcodec_ARRAY.enc([1, 2, 3, 4, 5]) + >>> encoded.hex() + '850102030405' + >>> + >>> # Decode the array + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> [item.val for item in decoded.val] + [1, 2, 3, 4, 5] + >>> + >>> # Encode a map + >>> encoded = CBORcodec_MAP.enc({"x": 100, "y": 200}) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> isinstance(decoded, CBOR_MAP) + True + +Working with different types +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +CBOR supports various data types:: + + >>> # Booleans + >>> true_val = CBOR_TRUE() + >>> false_val = CBOR_FALSE() + >>> bytes(true_val).hex() + 'f5' + >>> bytes(false_val).hex() + 'f4' + >>> + >>> # Null and undefined + >>> null_val = CBOR_NULL() + >>> undef_val = CBOR_UNDEFINED() + >>> bytes(null_val).hex() + 'f6' + >>> bytes(undef_val).hex() + 'f7' + >>> + >>> # Floating point + >>> float_val = CBOR_FLOAT(3.14159) + >>> bytes(float_val).hex() + 'fb400921f9f01b866e' + >>> + >>> # Negative integers + >>> neg = CBOR_NEGATIVE_INTEGER(-100) + >>> bytes(neg).hex() + '3863' + +Complex structures +^^^^^^^^^^^^^^^^^^ + +CBOR supports nested structures:: + + >>> # Nested arrays + >>> nested = CBORcodec_ARRAY.enc([1, [2, 3], [4, [5, 6]]]) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(nested) + >>> isinstance(decoded, CBOR_ARRAY) + True + >>> + >>> # Complex maps with mixed types + >>> data = { + ... "name": "Bob", + ... "age": 25, + ... "active": True, + ... "tags": ["user", "admin"] + ... } + >>> encoded = CBORcodec_MAP.enc(data) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> len(decoded.val) + 4 + +Semantic tags +^^^^^^^^^^^^^ + +CBOR supports semantic tags (major type 6) for providing additional meaning to data items:: + + >>> # Tag 1 is for Unix epoch timestamps + >>> import time + >>> timestamp = int(time.time()) + >>> tagged = CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(timestamp))) + >>> encoded = bytes(tagged) + >>> decoded, _ = CBOR_Codecs.CBOR.dec(encoded) + >>> decoded.val[0] # Tag number + 1 + +Interoperability +^^^^^^^^^^^^^^^^ + +Scapy's CBOR implementation is fully interoperable with other CBOR libraries. The implementation has been tested with the ``cbor2`` Python library to ensure RFC 8949 compliance:: + + >>> import cbor2 + >>> # Encode with Scapy, decode with cbor2 + >>> scapy_obj = CBOR_UNSIGNED_INTEGER(12345) + >>> scapy_encoded = bytes(scapy_obj) + >>> cbor2.loads(scapy_encoded) + 12345 + >>> + >>> # Encode with cbor2, decode with Scapy + >>> cbor2_encoded = cbor2.dumps([1, "test", True]) + >>> scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) + >>> isinstance(scapy_decoded, CBOR_ARRAY) + True + +Error handling +^^^^^^^^^^^^^^ + +Scapy provides safe decoding with error handling:: + + >>> # Safe decoding returns error objects for invalid data + >>> invalid_data = b'\xff\xff\xff' + >>> obj, remainder = CBOR_Codecs.CBOR.safedec(invalid_data) + >>> isinstance(obj, CBOR_DECODING_ERROR) + True + From 0548e4d2cad5f45f7d24fbf968e2ef072a80ca3e Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 11 Feb 2026 22:00:43 +0100 Subject: [PATCH 5/6] Fix codacy issues --- scapy/cbor/cbor.py | 3 ++- scapy/cbor/cborcodec.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 80fbcb56610..b2f2025079c 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -21,6 +21,7 @@ TYPE_CHECKING, ) +from build.lib.scapy.error import log_runtime from scapy.compat import plain_str from scapy.error import Scapy_Exception from scapy.utils import Enum_metaclass, EnumElement @@ -169,7 +170,7 @@ def __new__(cls, try: c.tag.register_cbor_object(c) except Exception: - pass # Some objects may not have tags yet + log_runtime.error("Failed to register codec for tag") return c diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 18d342993cc..b49b9d38b30 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -32,6 +32,7 @@ _CBOR_ERROR, ) from scapy.compat import chb, orb +from scapy.error import log_runtime ################## @@ -153,7 +154,7 @@ def __new__(cls, try: c.tag.register(c.codec, c) except Exception: - pass # Some codecs may not have tags yet + log_runtime.error("Failed to register codec for tag") return c From f04ae791a4a5b344f5fee5fb35958163f58d8fa1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:10:15 +0000 Subject: [PATCH 6/6] Fix UTF-8 encoding test failures on Windows by specifying encoding in file opens Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- scapy/tools/UTscapy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scapy/tools/UTscapy.py b/scapy/tools/UTscapy.py index b4761c5b8fc..e50c8346f13 100644 --- a/scapy/tools/UTscapy.py +++ b/scapy/tools/UTscapy.py @@ -330,7 +330,7 @@ def parse_config_file(config_path, verb=3): } """ - with open(config_path) as config_file: + with open(config_path, encoding='utf-8') as config_file: data = json.load(config_file) if verb > 2: print(" %s Loaded config file" % arrow, config_path) @@ -473,7 +473,7 @@ def compute_campaign_digests(test_campaign): ts.crc = crc32(dts) dc += "\0\x01" + dts test_campaign.crc = crc32(dc) - with open(test_campaign.filename) as fdesc: + with open(test_campaign.filename, encoding='utf-8') as fdesc: test_campaign.sha = sha1(fdesc.read()) @@ -1185,7 +1185,7 @@ def main(): if VERB > 2: print(theme.green(dash + " Loading: %s" % TESTFILE)) PREEXEC = PREEXEC_DICT[TESTFILE] if TESTFILE in PREEXEC_DICT else GLOB_PREEXEC - with open(TESTFILE) as testfile: + with open(TESTFILE, encoding='utf-8') as testfile: output, result, campaign = execute_campaign( testfile, OUTPUTFILE, PREEXEC, NUM, KW_OK, KW_KO, DUMP, DOCS, FORMAT, VERB, ONLYFAILED, CRC, INTERPRETER,