synta 0.2.4

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
# 34. `example_schema.py` — ASN.1 structure definitions with `synta.schema`

[← Example index](index.md) · [`example_schema.py` on Codeberg](https://codeberg.org/abbra/synta/src/branch/main/examples/example_schema.py)

Bindings: `synta.schema.asn1_sequence`, `synta.schema.asn1_choice`,
`synta.schema.asn1_field`.

- Define a CHOICE type (`Time`) with `@asn1_choice` over `UtcTime` and
  `GeneralizedTime`; encode with `to_der()`, decode with `from_der()`, and
  verify that the active variant is preserved.
- Define a simple SEQUENCE (`Validity`) with `@asn1_sequence` using the CHOICE
  type as a nested field; verify the round-trip and that `@dataclass` `__eq__`
  and `__repr__` continue to work.
- Define a SEQUENCE with an optional `[0] EXPLICIT` tagged field and an optional
  `[2] IMPLICIT` tagged field using `asn1_field()`; show that absent fields are
  omitted from the encoding and decoded back as `None`.
- Define a SEQUENCE with a `List[synta.IA5String]` field; encode a list of two
  names, decode back, and verify element count and values.
- Show `ValueError` when encoding a CHOICE instance with all fields `None`.
- Show `ValueError` when encoding a SEQUENCE with a required field set to `None`.
- Show `TypeError` when applying `@asn1_sequence` before `@dataclass`.

## Source

```python
#!/usr/bin/env python3
"""
Example 34: ASN.1 structure definitions with synta.schema.

Demonstrates: asn1_sequence, asn1_choice, asn1_field — define ASN.1 SEQUENCE
and CHOICE types in Python using class decorators and get to_der() / from_der()
automatically.
"""

from __future__ import annotations

import dataclasses
from dataclasses import dataclass, field
from typing import List, Optional

import synta
from synta.schema import asn1_choice, asn1_field, asn1_sequence


def section(title: str) -> None:
    print(f"\n{'─' * 60}\n{title}\n{'─' * 60}")


# ---------------------------------------------------------------------------
# Type definitions (shared across demos)
# ---------------------------------------------------------------------------

@asn1_choice
@dataclass
class Time:
    """ASN.1 Time CHOICE — either UTCTime or GeneralizedTime."""
    utc_time:         synta.UtcTime         | None = None
    generalized_time: synta.GeneralizedTime | None = None


@asn1_sequence
@dataclass
class Validity:
    """ASN.1 Validity SEQUENCE — two Time values."""
    not_before: Time
    not_after:  Time


@asn1_sequence
@dataclass
class SubjectAltNames:
    """SEQUENCE OF IA5String — a list of DNS names."""
    names: List[synta.IA5String] = field(default_factory=list)


@asn1_sequence
@dataclass
class TBSSketch:
    """Simplified TBS sketch with optional tagged fields."""
    serial:   synta.Integer
    validity: Validity
    # [0] EXPLICIT version — present only when not None
    version:  Optional[synta.Integer]     = asn1_field(tag=0, explicit=True)
    # [2] IMPLICIT key identifier — present only when not None
    key_id:   Optional[synta.OctetString] = asn1_field(tag=2, implicit=True)


# ---------------------------------------------------------------------------
# Demo functions
# ---------------------------------------------------------------------------

def demo_choice_basic():
    section("1. CHOICE type — UTCTime variant")
    t = Time(utc_time=synta.UtcTime(2024, 6, 1, 12, 0, 0))
    der = t.to_der()
    print(f"  DER (UTCTime variant): {der.hex()}")

    t2 = Time.from_der(der)
    assert t2.utc_time is not None
    assert t2.generalized_time is None
    assert t2.utc_time.year == 2024
    print(f"  Decoded year: {t2.utc_time.year}  (expected 2024)")
    print("  UTCTime variant round-trip: OK")


def demo_choice_generalized():
    section("2. CHOICE type — GeneralizedTime variant")
    t = Time(generalized_time=synta.GeneralizedTime(2099, 12, 31, 23, 59, 59))
    der = t.to_der()
    print(f"  DER (GeneralizedTime variant): {der.hex()}")

    t2 = Time.from_der(der)
    assert t2.utc_time is None
    assert t2.generalized_time is not None
    assert t2.generalized_time.year == 2099
    print(f"  Decoded year: {t2.generalized_time.year}  (expected 2099)")
    print("  GeneralizedTime variant round-trip: OK")


def demo_nested_sequence():
    section("3. SEQUENCE with nested CHOICE fields")
    v = Validity(
        not_before=Time(utc_time=synta.UtcTime(2024, 1, 1, 0, 0, 0)),
        not_after=Time(utc_time=synta.UtcTime(2025, 1, 1, 0, 0, 0)),
    )
    print(f"  repr: {v!r}")

    der = v.to_der()
    print(f"  DER: {der.hex()}")

    v2 = Validity.from_der(der)
    assert v == v2          # @dataclass __eq__
    assert v2.not_before.utc_time.year == 2024
    assert v2.not_after.utc_time.year == 2025
    print("  Nested SEQUENCE round-trip: OK")
    print(f"  v == v2: {v == v2}  (dataclass __eq__)")


def demo_optional_tagged_fields():
    section("4. Optional and tagged fields")
    # Both optional fields absent
    tbs = TBSSketch(
        serial=synta.Integer(1),
        validity=Validity(
            not_before=Time(utc_time=synta.UtcTime(2024, 1, 1, 0, 0, 0)),
            not_after=Time(utc_time=synta.UtcTime(2025, 1, 1, 0, 0, 0)),
        ),
    )
    der_without = tbs.to_der()
    tbs2 = TBSSketch.from_der(der_without)
    assert tbs2.version is None
    assert tbs2.key_id is None
    print("  Both optional fields absent — decoded as None: OK")

    # With [0] EXPLICIT version and [2] IMPLICIT key_id
    tbs_full = TBSSketch(
        serial=synta.Integer(42),
        validity=Validity(
            not_before=Time(utc_time=synta.UtcTime(2024, 3, 1, 0, 0, 0)),
            not_after=Time(utc_time=synta.UtcTime(2025, 3, 1, 0, 0, 0)),
        ),
        version=synta.Integer(2),
        key_id=synta.OctetString(b"\xde\xad\xbe\xef"),
    )
    der_full = tbs_full.to_der()
    tbs3 = TBSSketch.from_der(der_full)
    assert tbs3.version.to_int() == 2
    assert tbs3.key_id.to_bytes() == b"\xde\xad\xbe\xef"
    print(f"  [0] EXPLICIT version: {tbs3.version.to_int()}  (expected 2)")
    print(f"  [2] IMPLICIT key_id: {tbs3.key_id.to_bytes().hex()}  (expected deadbeef)")
    print("  Optional tagged fields round-trip: OK")


def demo_sequence_of():
    section("5. SEQUENCE OF (List field)")
    san = SubjectAltNames(names=[
        synta.IA5String("example.com"),
        synta.IA5String("www.example.com"),
    ])
    der = san.to_der()
    print(f"  DER: {der.hex()}")

    san2 = SubjectAltNames.from_der(der)
    assert len(san2.names) == 2
    assert san2.names[0].as_str() == "example.com"
    assert san2.names[1].as_str() == "www.example.com"
    print(f"  names[0]: {san2.names[0].as_str()!r}")
    print(f"  names[1]: {san2.names[1].as_str()!r}")
    print("  SEQUENCE OF round-trip: OK")

    # Empty list
    empty = SubjectAltNames()
    empty2 = SubjectAltNames.from_der(empty.to_der())
    assert empty2.names == []
    print("  Empty SEQUENCE OF round-trip: OK")


def demo_dataclass_features():
    section("6. Dataclass features are preserved")
    v = Validity(
        not_before=Time(utc_time=synta.UtcTime(2024, 1, 1, 0, 0, 0)),
        not_after=Time(utc_time=synta.UtcTime(2025, 1, 1, 0, 0, 0)),
    )
    # __repr__
    r = repr(v)
    assert "Validity" in r
    print(f"  repr: {r[:60]}...")
    # __eq__
    v2 = Validity.from_der(v.to_der())
    assert v == v2
    print(f"  v == v2 (after round-trip): True")
    # dataclasses.fields()
    field_names = [f.name for f in dataclasses.fields(v)]
    assert field_names == ["not_before", "not_after"]
    print(f"  dataclasses.fields: {field_names}")


def demo_error_all_none_choice():
    section("7. ValueError — CHOICE with all fields None")
    t = Time()   # all fields default to None
    try:
        t.to_der()
        print("  No error (unexpected)")
    except ValueError as e:
        print(f"  ValueError: {e}")


def demo_error_required_none():
    section("8. ValueError — required SEQUENCE field is None")
    # Construct with serial=None bypassing dataclass type checking
    tbs = TBSSketch.__new__(TBSSketch)
    object.__setattr__(tbs, "serial", None)
    object.__setattr__(tbs, "validity", Validity(
        not_before=Time(utc_time=synta.UtcTime(2024, 1, 1, 0, 0, 0)),
        not_after=Time(utc_time=synta.UtcTime(2025, 1, 1, 0, 0, 0)),
    ))
    object.__setattr__(tbs, "version", None)
    object.__setattr__(tbs, "key_id", None)
    try:
        tbs.to_der()
        print("  No error (unexpected)")
    except ValueError as e:
        print(f"  ValueError: {e}")


def demo_error_wrong_decorator_order():
    section("9. TypeError — @asn1_sequence applied before @dataclass")
    try:
        @dataclass
        @asn1_sequence          # applied first — wrong order
        class Bad:
            x: synta.Integer
        print("  No error (unexpected)")
    except TypeError as e:
        print(f"  TypeError: {e}")


def main():
    print("=" * 60)
    print("Example 34: synta.schema — ASN.1 structure definitions")
    print("=" * 60)
    demo_choice_basic()
    demo_choice_generalized()
    demo_nested_sequence()
    demo_optional_tagged_fields()
    demo_sequence_of()
    demo_dataclass_features()
    demo_error_all_none_choice()
    demo_error_required_none()
    demo_error_wrong_decorator_order()
    print("\nAll synta.schema examples completed.")


if __name__ == "__main__":
    main()
```