synta 0.1.9

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
#!/usr/bin/env python3
"""
Example 17: Advanced Encoder operations.

Demonstrates: Encoder.encode_implicit_tag, Encoder.encode_set,
Encoder.encode_explicit_tag, Encoder.encode_sequence,
and all _object variants not demonstrated elsewhere.
"""

import synta


def section(title):
    print(f"\n{'' * 60}\n{title}\n{'' * 60}")


def demo_encode_set():
    section("encode_set — build a SET containing two PrintableStrings")
    inner = synta.Encoder(synta.Encoding.DER)
    inner.encode_printable_string("Alice")
    inner.encode_printable_string("Bob")
    inner_bytes = inner.finish()

    outer = synta.Encoder(synta.Encoding.DER)
    outer.encode_set(inner_bytes)
    data = outer.finish()
    print(f"  SET DER: {data.hex()}")
    assert data[0] == 0x31  # SET tag

    # Decode to verify
    dec = synta.Decoder(data, synta.Encoding.DER)
    child = dec.decode_set()
    s1 = child.decode_printable_string()
    s2 = child.decode_printable_string()
    assert child.is_empty()
    print(f"  Decoded: {s1.as_str()!r}, {s2.as_str()!r}")


def demo_encode_implicit_tag():
    section("encode_implicit_tag — [0] IMPLICIT IA5String dNSName")
    # In X.509 GeneralName, dNSName is [2] IMPLICIT IA5String
    name = b"example.com"
    enc = synta.Encoder(synta.Encoding.DER)
    enc.encode_implicit_tag(2, "Context", False, name)  # False = primitive (IA5String)
    data = enc.finish()
    print(f"  DER: {data.hex()}")
    assert data[0] == 0x82  # Context[2] primitive
    assert data[1] == len(name)
    assert data[2:] == name
    print(f"  Tag: 0x{data[0]:02x} (Context[2] primitive)")

    # Decode back using decode_implicit_tag
    dec = synta.Decoder(data, synta.Encoding.DER)
    child = dec.decode_implicit_tag(2, "Context")
    raw = child.remaining_bytes()
    assert raw == name
    print(f"  Decoded dNSName: {raw.decode('ascii')!r}")


def demo_encode_explicit_tag():
    section("encode_explicit_tag — nested SEQUENCE { [0] EXPLICIT SEQUENCE { INTEGER } }")
    # Build the innermost INTEGER
    int_enc = synta.Encoder(synta.Encoding.DER)
    int_enc.encode_integer(42)
    int_bytes = int_enc.finish()

    # Wrap in SEQUENCE
    seq_enc = synta.Encoder(synta.Encoding.DER)
    seq_enc.encode_sequence(int_bytes)
    seq_bytes = seq_enc.finish()

    # Wrap in [0] EXPLICIT (Context)
    tag_enc = synta.Encoder(synta.Encoding.DER)
    tag_enc.encode_explicit_tag(0, "Context", seq_bytes)
    tag_bytes = tag_enc.finish()

    # Wrap in outer SEQUENCE
    outer_enc = synta.Encoder(synta.Encoding.DER)
    outer_enc.encode_sequence(tag_bytes)
    data = outer_enc.finish()
    print(f"  SEQUENCE {{ [0] EXPLICIT SEQUENCE {{ INTEGER 42 }} }}")
    print(f"  DER: {data.hex()}")

    # Decode and verify
    dec = synta.Decoder(data, synta.Encoding.DER)
    outer = dec.decode_sequence()
    tagged = outer.decode_explicit_tag(0)  # strips [0] EXPLICIT
    inner = tagged.decode_sequence()
    val = inner.decode_integer()
    assert val.to_int() == 42
    print(f"  Decoded INTEGER: {val.to_int()}")
    print("  Nested encoding/decoding: OK")


def demo_object_variants():
    section("_object encode variants — typed objects from decode")
    # Encode typed objects back without re-specifying their raw bytes.
    # Collect one object of each type via decode, then re-encode.
    enc = synta.Encoder(synta.Encoding.DER)
    enc.encode_integer(7)
    enc.encode_boolean(True)
    enc.encode_octet_string(b"hi")
    enc.encode_bit_string(synta.BitString(b"\xff", 0))
    enc.encode_null()
    enc.encode_utf8_string("test")
    enc.encode_printable_string("ok")
    enc.encode_ia5_string("ia5")
    enc.encode_numeric_string("123")
    enc.encode_teletex_string(b"teletex")
    enc.encode_visible_string("visible")
    enc.encode_general_string(b"general")
    enc.encode_universal_string("uni")
    enc.encode_bmp_string("bmp")
    enc.encode_oid(synta.ObjectIdentifier("1.2.3"))
    enc.encode_utc_time(synta.UtcTime(2026, 1, 1, 0, 0, 0))
    enc.encode_generalized_time(synta.GeneralizedTime(2026, 1, 1, 0, 0, 0, None))
    original = enc.finish()

    dec = synta.Decoder(original, synta.Encoding.DER)
    obj_int    = dec.decode_integer()
    obj_bool   = dec.decode_boolean()
    obj_oct    = dec.decode_octet_string()
    obj_bit    = dec.decode_bit_string()
    obj_null   = dec.decode_null()
    obj_utf8   = dec.decode_utf8_string()
    obj_print  = dec.decode_printable_string()
    obj_ia5    = dec.decode_ia5_string()
    obj_num    = dec.decode_numeric_string()
    obj_tel    = dec.decode_teletex_string()
    obj_vis    = dec.decode_visible_string()
    obj_gen    = dec.decode_general_string()
    obj_uni    = dec.decode_universal_string()
    obj_bmp    = dec.decode_bmp_string()
    obj_oid    = dec.decode_oid()
    obj_utc    = dec.decode_utc_time()
    obj_gt     = dec.decode_generalized_time()
    assert dec.is_empty()

    # Re-encode using _object variants
    enc2 = synta.Encoder(synta.Encoding.DER)
    enc2.encode_integer_object(obj_int)
    enc2.encode_boolean_object(obj_bool)
    enc2.encode_octet_string_object(obj_oct)
    enc2.encode_bit_string_object(obj_bit)
    enc2.encode_null_object(obj_null)
    enc2.encode_utf8_string_object(obj_utf8)
    enc2.encode_printable_string_object(obj_print)
    enc2.encode_ia5_string_object(obj_ia5)
    enc2.encode_numeric_string_object(obj_num)
    enc2.encode_teletex_string_object(obj_tel)
    enc2.encode_visible_string_object(obj_vis)
    enc2.encode_general_string_object(obj_gen)
    enc2.encode_universal_string_object(obj_uni)
    enc2.encode_bmp_string_object(obj_bmp)
    enc2.encode_oid_object(obj_oid)
    enc2.encode_utc_time_object(obj_utc)
    enc2.encode_generalized_time_object(obj_gt)
    reconstructed = enc2.finish()

    assert reconstructed == original, (
        f"Mismatch:\n  orig: {original.hex()}\n  got:  {reconstructed.hex()}"
    )
    print(f"  All _object variants round-trip ({len(original)} bytes): OK")


def demo_application_and_private_tags():
    section("encode_explicit_tag — Application and Private tag classes")
    inner = synta.Encoder(synta.Encoding.DER)
    inner.encode_integer(1)
    inner_bytes = inner.finish()

    for tag_class in ("Application", "Private"):
        enc = synta.Encoder(synta.Encoding.DER)
        enc.encode_explicit_tag(3, tag_class, inner_bytes)
        data = enc.finish()
        print(f"  {tag_class}[3] EXPLICIT INTEGER 1: {data.hex()}")

    # Invalid class raises ValueError
    try:
        enc = synta.Encoder(synta.Encoding.DER)
        enc.encode_explicit_tag(0, "BadClass", inner_bytes)
        print("  No error (unexpected)")
    except ValueError as e:
        print(f"  ValueError for unknown tag class: {e}")


def main():
    print("=" * 60)
    print("Example 17: Advanced Encoder operations")
    print("=" * 60)
    demo_encode_set()
    demo_encode_implicit_tag()
    demo_encode_explicit_tag()
    demo_object_variants()
    demo_application_and_private_tags()
    print("\nAll advanced Encoder examples completed.")


if __name__ == "__main__":
    main()