diff --git a/examples/pyasn1/README.md b/examples/pyasn1/README.md new file mode 100644 index 0000000..691e946 --- /dev/null +++ b/examples/pyasn1/README.md @@ -0,0 +1,18 @@ +# pyasn1 Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/pyasn1/ber_cer_der_codecs/code.py b/examples/pyasn1/ber_cer_der_codecs/code.py new file mode 100644 index 0000000..b098fbe --- /dev/null +++ b/examples/pyasn1/ber_cer_der_codecs/code.py @@ -0,0 +1,84 @@ +# pyasn1 decouples ASN.1 types from serialization. The same value can +# be encoded with several codecs: BER (basic), CER (canonical, useful +# for streaming), and DER (canonical, used in X.509, PKCS, etc.). +from pyasn1.type.univ import ( + Integer, OctetString, ObjectIdentifier, Sequence, SequenceOf, +) +from pyasn1.type.char import UTF8String +from pyasn1.type.namedtype import NamedTypes, NamedType +from pyasn1.codec.ber.encoder import encode as ber_encode +from pyasn1.codec.cer.encoder import encode as cer_encode +from pyasn1.codec.der.encoder import encode as der_encode +from pyasn1.codec.der.decoder import decode as der_decode + + +def hexdump(data): + return " ".join(f"{b:02X}" for b in data) + + +# A SEQUENCE describing a tagged measurement: a sensor OID, a label, +# and an integer reading. +class Measurement(Sequence): + componentType = NamedTypes( + NamedType("sensor", ObjectIdentifier()), + NamedType("label", UTF8String()), + NamedType("reading", Integer()), + ) + + +reading = Measurement() +reading["sensor"] = "1.3.6.1.4.1.99999.1" # made-up enterprise OID +reading["label"] = "kitchen-thermometer" +reading["reading"] = 21 + +heading("One value, three encodings") +note( + "Each codec produces standards-compliant bytes. BER is the most " + "permissive; CER and DER are canonical (a given value has exactly " + "one valid encoding)." +) + +for name, encode in [("BER", ber_encode), ("CER", cer_encode), ("DER", der_encode)]: + substrate = encode(reading) + note(f"{name} ({len(substrate)} bytes):") + display(HTML(f"
{hexdump(substrate)}"), append=True)
+
+heading("A SEQUENCE OF Measurement")
+note(
+ "ASN.1 SEQUENCE OF is the natural fit for a homogeneous list. "
+ "We build three readings, encode the lot to DER, and round-trip "
+ "back to inspect the contents."
+)
+
+
+class MeasurementLog(SequenceOf):
+ componentType = Measurement()
+
+
+log = MeasurementLog()
+samples = [
+ ("1.3.6.1.4.1.99999.1", "kitchen-thermometer", 21),
+ ("1.3.6.1.4.1.99999.2", "garage-thermometer", 8),
+ ("1.3.6.1.4.1.99999.3", "attic-thermometer", 27),
+]
+for oid, label, value in samples:
+ item = Measurement()
+ item["sensor"] = oid
+ item["label"] = label
+ item["reading"] = value
+ log.append(item)
+
+substrate = der_encode(log)
+note(f"DER-encoded log is {len(substrate)} bytes.")
+display(HTML(f"{hexdump(substrate)}"), append=True)
+
+decoded_log, _ = der_decode(substrate, asn1Spec=MeasurementLog())
+rows = ["| sensor OID | label | reading |
|---|---|---|
{item['sensor']} | "
+ f"{item['label']} | " + f"{int(item['reading'])} |
{text}
"), append=True) + diff --git a/examples/pyasn1/encode_decode_record/code.py b/examples/pyasn1/encode_decode_record/code.py new file mode 100644 index 0000000..5fd9de2 --- /dev/null +++ b/examples/pyasn1/encode_decode_record/code.py @@ -0,0 +1,88 @@ +""" +A first look at pyasn1: defining an ASN.1 SEQUENCE, populating it, +encoding it to DER bytes, and decoding it back into a Python object. + +The ASN.1 schema we model here is the classic introductory example: + + Record ::= SEQUENCE { + id INTEGER, + room [0] INTEGER OPTIONAL, + house [1] INTEGER DEFAULT 0 + } + +Docs: https://pyasn1.readthedocs.io/ +""" +from IPython.core.display import display, HTML +# pyasn1 imports used by this example. +from pyasn1.type.univ import Integer, Sequence +from pyasn1.type.namedtype import ( + NamedTypes, NamedType, OptionalNamedType, DefaultedNamedType, +) +from pyasn1.type.tag import Tag, tagClassContext, tagFormatSimple +from pyasn1.codec.der.encoder import encode as der_encode +from pyasn1.codec.der.decoder import decode as der_decode + + +def hexdump(data): + """Return a space-separated hex string of the bytes in `data`.""" + return " ".join(f"{b:02X}" for b in data) + + + +# Define the Record schema as a Python class. Each named type maps to a +# field in the SEQUENCE; the context-specific implicit tags ([0], [1]) +# are attached via .subtype(implicitTag=...). +class Record(Sequence): + componentType = NamedTypes( + NamedType("id", Integer()), + OptionalNamedType( + "room", + Integer().subtype( + implicitTag=Tag(tagClassContext, tagFormatSimple, 0) + ), + ), + DefaultedNamedType( + "house", + Integer(0).subtype( + implicitTag=Tag(tagClassContext, tagFormatSimple, 1) + ), + ), + ) + + +heading("Building a Record value") +note( + "We populate the SEQUENCE much like a dict, then ask pyasn1 for " + "its human-readable form via str()." +) + +record = Record() +record["id"] = 123 +record["room"] = 321 + +display(HTML(f"{str(record)}"), append=True)
+
+heading("Encoding to DER")
+note(
+ "DER (Distinguished Encoding Rules) gives us a compact, canonical "
+ "byte representation. Notice how the optional 'room' field is "
+ "included but the defaulted 'house' field is omitted."
+)
+
+substrate = der_encode(record)
+display(HTML(f"DER bytes: {hexdump(substrate)}"), append=True)
+display(HTML(f"Length: {len(substrate)} bytes"), append=True)
+
+heading("Decoding DER back into a Record")
+note(
+ "Pass the schema (asn1Spec=Record()) so the decoder knows how to "
+ "interpret the implicit tags. The defaulted 'house' field comes "
+ "back as 0 even though it was not present in the bytes."
+)
+
+decoded, leftover = der_decode(substrate, asn1Spec=Record())
+for field_name in ("id", "room", "house"):
+ note(f"{field_name} = {int(decoded[field_name])}")
+
+note(f"Leftover bytes after decoding: {len(leftover)}")
+note(f"Round-trip equal? {record == decoded}")
diff --git a/examples/pyasn1/encode_decode_record/config.toml b/examples/pyasn1/encode_decode_record/config.toml
new file mode 100644
index 0000000..3b0ab7c
--- /dev/null
+++ b/examples/pyasn1/encode_decode_record/config.toml
@@ -0,0 +1 @@
+packages = ["pyasn1"]
diff --git a/examples/pyasn1/encode_decode_record/setup.py b/examples/pyasn1/encode_decode_record/setup.py
new file mode 100644
index 0000000..419dff5
--- /dev/null
+++ b/examples/pyasn1/encode_decode_record/setup.py
@@ -0,0 +1,38 @@
+"""Shim setup for the first example. Includes the full IPython shim."""
+import sys
+import types
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ return _display(
+ *args, **kwargs, target=__pyscript_display_target__,
+ )
+
+
+ipython = types.ModuleType("IPython")
+core = types.ModuleType("IPython.core")
+core_display = types.ModuleType("IPython.core.display")
+core_display.display = display
+core_display.HTML = HTML
+ipython.core = core
+core.display = core_display
+ipython.version_info = (9, 0, 2, '')
+ipython.get_ipython = lambda: None
+ipython.display = core_display
+sys.modules["IPython"] = ipython
+sys.modules["IPython.core"] = core
+sys.modules["IPython.core.display"] = core_display
+sys.modules["IPython.display"] = core_display
+
+
+def heading(text, level=2):
+ display(HTML(f"{text}
"), append=True) + diff --git a/examples/pyasn1/native_python_bridge/code.py b/examples/pyasn1/native_python_bridge/code.py new file mode 100644 index 0000000..71840bc --- /dev/null +++ b/examples/pyasn1/native_python_bridge/code.py @@ -0,0 +1,95 @@ +# The "native" codec converts pyasn1 objects to plain Python dicts/lists +# and back. This is handy for moving data between an ASN.1-shaped wire +# format and the dict/list world of JSON, configs, and tests. + +from pyasn1.type.univ import Integer, Sequence, SequenceOf +from pyasn1.type.char import UTF8String +from pyasn1.type.namedtype import NamedTypes, NamedType, OptionalNamedType +from pyasn1.codec.der.encoder import encode as der_encode +from pyasn1.codec.der.decoder import decode as der_decode +from pyasn1.codec.native.encoder import encode as to_python +from pyasn1.codec.native.decoder import decode as from_python + + +def hexdump(data): + return " ".join(f"{b:02X}" for b in data) + + +class Address(Sequence): + componentType = NamedTypes( + NamedType("street", UTF8String()), + NamedType("city", UTF8String()), + OptionalNamedType("postcode", UTF8String()), + ) + + +class Contact(Sequence): + componentType = NamedTypes( + NamedType("name", UTF8String()), + NamedType("age", Integer()), + NamedType("address", Address()), + ) + + +class ContactBook(SequenceOf): + componentType = Contact() + + +heading("From Python dicts into pyasn1") +note( + "Start with ordinary Python data, then hand it to the native " + "decoder along with a schema. pyasn1 builds the corresponding " + "ASN.1 object tree." +) + +people = [ + { + "name": "Ada Lovelace", + "age": 36, + "address": {"street": "1 Analytical Way", "city": "London"}, + }, + { + "name": "Grace Hopper", + "age": 85, + "address": { + "street": "42 Compiler Ave", + "city": "Arlington", + "postcode": "22202", + }, + }, +] + +book = from_python(people, asn1Spec=ContactBook()) +display(HTML(f"{str(book)}"), append=True)
+
+heading("Round-tripping through DER")
+note(
+ "Encode to DER for the wire, decode back, then flatten to native "
+ "Python with the native encoder. Optional fields that were absent "
+ "stay absent in the output."
+)
+
+wire = der_encode(book)
+note(f"DER payload is {len(wire)} bytes.")
+display(HTML(f"{hexdump(wire)}"), append=True)
+
+restored, _ = der_decode(wire, asn1Spec=ContactBook())
+as_python = to_python(restored)
+
+# Render the recovered Python structure as an HTML table.
+rows = ["| name | age | city | postcode |
|---|---|---|---|
| {entry['name']} | " + f"{entry['age']} | " + f"{addr['city']} | " + f"{addr.get('postcode', '—')} |
pyasn1-modules to work with "
+ "real-world ASN.1 schemas like X.509 certificates, PKCS, and SNMP."
+)
diff --git a/examples/pyasn1/native_python_bridge/config.toml b/examples/pyasn1/native_python_bridge/config.toml
new file mode 100644
index 0000000..3b0ab7c
--- /dev/null
+++ b/examples/pyasn1/native_python_bridge/config.toml
@@ -0,0 +1 @@
+packages = ["pyasn1"]
diff --git a/examples/pyasn1/native_python_bridge/setup.py b/examples/pyasn1/native_python_bridge/setup.py
new file mode 100644
index 0000000..1646442
--- /dev/null
+++ b/examples/pyasn1/native_python_bridge/setup.py
@@ -0,0 +1,17 @@
+"""Lighter setup: same names as cell 1, no IPython shim."""
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ return _display(*args, **kwargs, target=__pyscript_display_target__)
+
+
+def heading(text, level=2):
+ display(HTML(f"{text}
"), append=True) diff --git a/examples/pyasn1/order.json b/examples/pyasn1/order.json new file mode 100644 index 0000000..8bbc9a3 --- /dev/null +++ b/examples/pyasn1/order.json @@ -0,0 +1,5 @@ +[ + "encode_decode_record", + "ber_cer_der_codecs", + "native_python_bridge" +]