synta 0.2.5

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
#!/usr/bin/env python3
"""
Example: Typed GeneralName API (synta.general_name).

Demonstrates the typed ``GeneralName`` objects returned by
``Certificate.subject_alt_names()`` and ``Certificate.general_names(oid)``,
plus the low-level ``synta.parse_general_names()`` function and the integer
tag constants in ``synta.general_name``.

The sample certificate has four Subject Alternative Names:
  - dNSName       "example.com"
  - dNSName       "www.example.com"
  - iPAddress     192.168.1.1  (4 bytes)
  - rfc822Name    "admin@example.com"
"""

import base64
import ipaddress

import synta
import synta.general_name as gn

# ── Sample certificate DER (self-signed, ECDSA/P-256) ─────────────────────────
#
# Subject / Issuer: CN=example.com, O=Example Corp, C=US
# SAN: dNSName=example.com, dNSName=www.example.com,
#      iPAddress=192.168.1.1, rfc822Name=admin@example.com

CERT_DER = base64.b64decode(
    "MIIB6DCCAY2gAwIBAgIUEBUQAc/E3r/3vioki6IU6lyfL5IwCgYIKoZIzj0EAwIw"
    "OjEUMBIGA1UEAwwLZXhhbXBsZS5jb20xFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEL"
    "MAkGA1UEBhMCVVMwHhcNMjUwMTAxMDAwMDAwWhcNMjcwMTAxMDAwMDAwWjA6MRQw"
    "EgYDVQQDDAtleGFtcGxlLmNvbTEVMBMGA1UECgwMRXhhbXBsZSBDb3JwMQswCQYD"
    "VQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLIerh5Q5OVsw1o/hfHd"
    "Hgi3mjz6WDirif1I+JAuF3oUPGa+iyMnngLjCgvIghAxvWNrcqp+/eewofR58P7X"
    "g6ujcTBvMEAGA1UdEQQ5MDeCC2V4YW1wbGUuY29tgg93d3cuZXhhbXBsZS5jb22H"
    "BMCoAQGBEWFkbWluQGV4YW1wbGUuY29tMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE"
    "FI8ICKdxcww2aHlOya4RvcbKlSVXMAoGCCqGSM49BAMCA0kAMEYCIQDrZBGKzs0V"
    "cEQ3uYzh9xNlKOGqZOjxct32A+dWJMlEdAIhAM0zkny+EHOqpQXKYbsOuxJedws6"
    "6d3nEZ7+v/kQ8hJP"
)

# Subject Alternative Name OID
SUBJECT_ALT_NAME = "2.5.29.17"


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


# ── Section 1: Typed subject_alt_names() ─────────────────────────────────────

def demo_typed_subject_alt_names():
    section("Typed subject_alt_names() — RFC 5280 §4.2.1.6")

    cert = synta.Certificate.from_der(CERT_DER)
    names = cert.subject_alt_names()

    # Four SANs in this certificate
    assert len(names) == 4, f"expected 4 SANs, got {len(names)}"

    for name in names:
        if isinstance(name, gn.DNSName):
            print(f"  dNSName:    {name.value}")
            assert name.value in ("example.com", "www.example.com")
        elif isinstance(name, gn.IPAddress):
            addr = ipaddress.ip_address(name.address)
            print(f"  iPAddress:  {addr}  ({len(name.address)} bytes)")
            assert name.address == b"\xc0\xa8\x01\x01"  # 192.168.1.1
        elif isinstance(name, gn.RFC822Name):
            print(f"  rfc822Name: {name.value}")
            assert name.value == "admin@example.com"

    print(f"  total SANs: {len(names)}")

    # repr() gives a readable summary for each type
    for name in names:
        print(f"  repr: {repr(name)}")


# ── Section 2: isinstance type-dispatch pattern ───────────────────────────────

def demo_type_dispatch():
    section("Type-dispatch pattern — all GeneralName variants")

    cert = synta.Certificate.from_der(CERT_DER)
    names = cert.subject_alt_names()

    print("  Full dispatch loop:")
    for name in names:
        if isinstance(name, gn.DNSName):
            print(f"    DNSName              → {name.value!r}")
        elif isinstance(name, gn.RFC822Name):
            print(f"    RFC822Name           → {name.value!r}")
        elif isinstance(name, gn.UniformResourceIdentifier):
            print(f"    URI                  → {name.value!r}")
        elif isinstance(name, gn.IPAddress):
            raw = name.address
            if len(raw) == 4:
                addr_str = str(ipaddress.IPv4Address(raw))
            elif len(raw) == 16:
                addr_str = str(ipaddress.IPv6Address(raw))
            else:
                addr_str = raw.hex()
            print(f"    IPAddress            → {addr_str}")
        elif isinstance(name, gn.RegisteredID):
            print(f"    RegisteredID         → {name.oid}")
        elif isinstance(name, gn.DirectoryName):
            attrs = synta.parse_name_attrs(name.name_der)
            print(f"    DirectoryName        → {attrs}")
        elif isinstance(name, gn.OtherName):
            print(f"    OtherName type_id    → {name.type_id}")
            print(f"    OtherName value      → {name.value.hex()}")
        elif isinstance(name, gn.X400Address):
            print(f"    X400Address raw_der  → {name.raw_der.hex()}")
        elif isinstance(name, gn.EDIPartyName):
            print(f"    EDIPartyName raw_der → {name.raw_der.hex()}")
        else:
            print(f"    unknown type: {type(name)}")


# ── Section 3: general_names() with explicit OID ──────────────────────────────

def demo_general_names_oid():
    section("general_names(oid) — decode any GeneralName extension by OID")

    cert = synta.Certificate.from_der(CERT_DER)

    # Pass OID as string — same result as subject_alt_names()
    names_str_oid = cert.general_names(SUBJECT_ALT_NAME)
    assert len(names_str_oid) == 4

    # Pass OID as ObjectIdentifier object
    oid_obj = synta.ObjectIdentifier(SUBJECT_ALT_NAME)
    names_obj_oid = cert.general_names(oid_obj)
    assert len(names_obj_oid) == 4

    print(f"  general_names({SUBJECT_ALT_NAME!r}) → {len(names_str_oid)} entries")
    print(f"  general_names(ObjectIdentifier(...)) → {len(names_obj_oid)} entries")

    # Absent extension → empty list (no exception)
    ISSUER_ALT_NAME = "2.5.29.18"
    ian = cert.general_names(ISSUER_ALT_NAME)
    assert ian == []
    print(f"  general_names({ISSUER_ALT_NAME!r}) → [] (absent extension)")

    # Spot-check the first DNS name
    dns_names = [n for n in names_str_oid if isinstance(n, gn.DNSName)]
    assert dns_names[0].value == "example.com"
    print(f"  first dNSName: {dns_names[0].value!r}")


# ── Section 4: Low-level parse_general_names() ───────────────────────────────

def demo_low_level_parse():
    section("Low-level synta.parse_general_names() — (tag_int, bytes) tuples")

    cert = synta.Certificate.from_der(CERT_DER)

    # get_extension_value_der() returns the raw OCTET STRING content of the
    # extension — i.e. the DER of the SEQUENCE OF GeneralName
    san_der = cert.get_extension_value_der(SUBJECT_ALT_NAME)
    assert san_der is not None, "SAN extension absent"
    print(f"  raw SAN extension DER: {len(san_der)} bytes")

    # parse_general_names() decodes that DER and returns (tag_int, value_bytes)
    # tuples — the original low-level API
    pairs = synta.parse_general_names(san_der)
    assert len(pairs) == 4

    for tag, value in pairs:
        print(f"  tag={tag}  value={value!r}")

    # Verify expected tag numbers
    tags = [tag for tag, _ in pairs]
    assert tags == [
        gn.DNS_NAME,    # 2
        gn.DNS_NAME,    # 2
        gn.IP_ADDRESS,  # 7
        gn.RFC822_NAME, # 1
    ]
    print("  tag order: DNS_NAME, DNS_NAME, IP_ADDRESS, RFC822_NAME ✓")

    # Decode an IP address from the raw bytes
    ip_pairs = [(t, v) for t, v in pairs if t == gn.IP_ADDRESS]
    assert len(ip_pairs) == 1
    _, ip_bytes = ip_pairs[0]
    addr = ipaddress.ip_address(ip_bytes)
    assert str(addr) == "192.168.1.1"
    print(f"  decoded IP: {addr}")


# ── Section 5: Integer tag constants ─────────────────────────────────────────

def demo_tag_constants():
    section("synta.general_name integer tag constants")

    constants = [
        ("OTHER_NAME",    gn.OTHER_NAME,    0),
        ("RFC822_NAME",   gn.RFC822_NAME,   1),
        ("DNS_NAME",      gn.DNS_NAME,      2),
        ("X400_ADDRESS",  gn.X400_ADDRESS,  3),
        ("DIRECTORY_NAME", gn.DIRECTORY_NAME, 4),
        ("EDI_PARTY_NAME", gn.EDI_PARTY_NAME, 5),
        ("URI",           gn.URI,           6),
        ("IP_ADDRESS",    gn.IP_ADDRESS,    7),
        ("REGISTERED_ID", gn.REGISTERED_ID, 8),
    ]

    for name, value, expected in constants:
        assert value == expected, f"{name}: expected {expected}, got {value}"
        print(f"  gn.{name} = {value}")

    # Using constants in a dispatch loop (alternative to isinstance)
    cert = synta.Certificate.from_der(CERT_DER)
    san_der = cert.get_extension_value_der(SUBJECT_ALT_NAME)
    assert san_der is not None
    pairs = synta.parse_general_names(san_der)
    print()
    print("  Low-level dispatch using tag constants:")
    for tag, value in pairs:
        if tag == gn.DNS_NAME:
            print(f"    DNS_NAME   → {value.decode()!r}")
        elif tag == gn.IP_ADDRESS:
            print(f"    IP_ADDRESS → {ipaddress.ip_address(value)}")
        elif tag == gn.RFC822_NAME:
            print(f"    RFC822_NAME → {value.decode()!r}")


# ── main ──────────────────────────────────────────────────────────────────────

def main():
    print("=" * 60)
    print("Example: Typed GeneralName API (synta.general_name)")
    print("=" * 60)
    demo_typed_subject_alt_names()
    demo_type_dispatch()
    demo_general_names_oid()
    demo_low_level_parse()
    demo_tag_constants()
    print("\nAll GeneralName examples completed.")


if __name__ == "__main__":
    main()