synta 0.1.3

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
#!/usr/bin/env python3
"""
Example 3: Extension access and SAN parsing.

Demonstrates: Certificate.subject_alt_names, Certificate.extensions_der,
Certificate.get_extension_value_der, synta.general_name tag constants,
synta.parse_general_names, synta.oids.SUBJECT_ALT_NAME,
synta.oids.BASIC_CONSTRAINTS, synta.oids.SUBJECT_KEY_IDENTIFIER,
Decoder.decode_sequence, Decoder.peek_tag, Decoder.decode_implicit_tag,
Decoder.decode_explicit_tag, Decoder.remaining_bytes, Decoder.decode_raw_tlv,
Decoder.is_empty.
"""

import base64
import ipaddress
import socket
import synta
import synta.general_name as gn
import synta.oids as oids

# ECDSA P-256 certificate with SAN (dNSName x2, iPAddress, rfc822Name)
# SubjectAltName: example.com, www.example.com, 192.168.1.1, admin@example.com
_SAN_CERT_B64 = (
    "MIIB6DCCAY2gAwIBAgIUEBUQAc/E3r/3vioki6IU6lyfL5IwCgYIKoZIzj0EAwIw"
    "OjEUMBIGA1UEAwwLZXhhbXBsZS5jb20xFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEL"
    "MAkGA1UEBhMCVVMwHhcNMjUwMTAxMDAwMDAwWhcNMjcwMTAxMDAwMDAwWjA6MRQw"
    "EgYDVQQDDAtleGFtcGxlLmNvbTEVMBMGA1UECgwMRXhhbXBsZSBDb3JwMQswCQYD"
    "VQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLIerh5Q5OVsw1o/hfHd"
    "Hgi3mjz6WDirif1I+JAuF3oUPGa+iyMnngLjCgvIghAxvWNrcqp+/eewofR58P7X"
    "g6ujcTBvMEAGA1UdEQQ5MDeCC2V4YW1wbGUuY29tgg93d3cuZXhhbXBsZS5jb22H"
    "BMCoAQGBEWFkbWluQGV4YW1wbGUuY29tMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE"
    "FI8ICKdxcww2aHlOya4RvcbKlSVXMAoGCCqGSM49BAMCA0kAMEYCIQDrZBGKzs0V"
    "cEQ3uYzh9xNlKOGqZOjxct32A+dWJMlEdAIhAM0zkny+EHOqpQXKYbsOuxJedws6"
    "6d3nEZ7+v/kQ8hJP"
)

# RSA cert with BasicConstraints CA:TRUE and SubjectKeyIdentifier
_RSA_CERT_B64 = (
    "MIIDiTCCAnGgAwIBAgIUXhaeS3ad5SJp60GRJU73OQaO0xkwDQYJKoZIhvcNAQEL"
    "BQAwVDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh"
    "bmNpc2NvMQ0wCwYDVQQKDARUZXN0MREwDwYDVQQDDAh0ZXN0LmNvbTAeFw0yNjAy"
    "MjMxMDU0MzRaFw0yNzAyMjMxMDU0MzRaMFQxCzAJBgNVBAYTAlVTMQswCQYDVQQI"
    "DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwEVGVzdDERMA8G"
    "A1UEAwwIdGVzdC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP"
    "9oir0NwIXFZ6gOUo//akzjNjvUhA/V1KSUY0L/iXOGWRHFcdcf4gVhoCgR+DgCV6"
    "bJKKrYPIvfEwmd8DdpPj1WU4Dztb4NNLgxquFZym2Swe0xDLQdtWoIQYerF/ER8D"
    "9Pk0qQ5QVaCO+KB3UKyXiJwcTc/LJnDqEX24mrf0ZH/HqB2GsUE3aI9aW5Lgwm9A"
    "7+gV7FrumaT7fQqpfNucWwlXU2SIRm//JKUrT0MGrh99vmmkGRZK+c9wLfIK+pny"
    "UQxSD1E395bpQTqTWIfcMWti6af3ix3GsWeoXwY+GDfZlZ1w22GjLmSgg1RMhhKZ"
    "9l+QFnI/GtmiXX2pCRZfAgMBAAGjUzBRMB0GA1UdDgQWBBRCjPvAUpiRe0Zs6DTL"
    "K+KLoTZfezAfBgNVHSMEGDAWgBRCjPvAUpiRe0Zs6DTLK+KLoTZfezAPBgNVHRMB"
    "Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC+xBpTScGlcZGYvwtWLZuF2qRu"
    "o9xhYzHXgIDJglXZ8i70O+ut2WRyI9RJOSMsa7BI2qmc87Ki9ZMCO2QIMQCQo7cB"
    "kZtQvK8iGGHhSwepMORekzdbfUUs7N4YEM3Xako1+4RzL+T1Z3qzQ6nrnQ+gYyQo"
    "GiIFKbYZONu2OlqXGQe5LPwbsPU52GUbttkxodaHgCdP7yKO/l3sDifXpaFEXJhY"
    "5RZqgSG77jymh8YKcY0X9J53OtP1So/IOS1Za137k+eYci6iCB4w511qEhSRZdCy"
    "+3NEoGrBearMcJHttGbMplR6TU6fDFRIUAdelPdWfpfmtl1iElRDwNdQkBBR"
)


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


def demo_extensions_der():
    section("extensions_der — raw SEQUENCE OF Extension bytes")
    cert = synta.Certificate.from_der(base64.b64decode(_SAN_CERT_B64))
    ext_der = cert.extensions_der
    assert ext_der is not None
    print(f"  extensions_der: <{len(ext_der)} bytes>")

    # Iterate extensions manually: each Extension is SEQUENCE { OID, [BOOLEAN], OCTET STRING }
    dec = synta.Decoder(ext_der, synta.Encoding.DER)
    ext_seq = dec.decode_sequence()          # SEQUENCE OF
    count = 0
    while not ext_seq.is_empty():
        ext = ext_seq.decode_sequence()      # Extension SEQUENCE
        oid = ext.decode_oid()
        # Optional critical BOOLEAN
        tag_no, tag_class, _ = ext.peek_tag()
        critical = False
        if tag_no == 1 and tag_class == "Universal":
            critical = ext.decode_boolean().value()
        value_bytes = ext.decode_octet_string()  # extnValue OCTET STRING
        print(f"  [{count}] OID={oid}  critical={critical}  value={len(value_bytes.to_bytes())} bytes")
        count += 1


def demo_get_extension_value_der():
    section("get_extension_value_der — look up by OID")
    cert = synta.Certificate.from_der(base64.b64decode(_RSA_CERT_B64))

    bc = cert.get_extension_value_der(oids.BASIC_CONSTRAINTS)
    print(f"  BasicConstraints value hex: {bc.hex()}")

    ski = cert.get_extension_value_der(oids.SUBJECT_KEY_IDENTIFIER)
    print(f"  SubjectKeyIdentifier value hex: {ski.hex()}")

    # OID not present → None
    san = cert.get_extension_value_der(oids.SUBJECT_ALT_NAME)
    assert san is None
    print(f"  SubjectAltName (absent):  None")


def demo_parse_basic_constraints():
    section("Parse BasicConstraints — SEQUENCE { BOOLEAN TRUE }")
    cert = synta.Certificate.from_der(base64.b64decode(_RSA_CERT_B64))
    bc_der = cert.get_extension_value_der(oids.BASIC_CONSTRAINTS)
    dec = synta.Decoder(bc_der, synta.Encoding.DER)
    seq = dec.decode_sequence()
    is_ca = seq.decode_boolean().value()
    print(f"  cA BOOLEAN: {is_ca}")
    assert is_ca is True
    assert seq.is_empty()


def demo_subject_alt_names():
    section("Certificate.subject_alt_names() — high-level SAN access")
    cert = synta.Certificate.from_der(base64.b64decode(_SAN_CERT_B64))

    # One call returns all GeneralName entries as (tag_number, content_bytes) tuples.
    # Use synta.general_name constants for readable dispatch — no magic numbers.
    for tag_num, content in cert.subject_alt_names():
        if tag_num == gn.DNS_NAME:
            print(f"  dNSName:   {content.decode('ascii')}")
        elif tag_num == gn.IP_ADDRESS:
            print(f"  iPAddress: {ipaddress.ip_address(content)}")
        elif tag_num == gn.RFC822_NAME:
            print(f"  email:     {content.decode('ascii')}")
        elif tag_num == gn.DIRECTORY_NAME:
            attrs = synta.parse_name_attrs(content)
            print(f"  DirName:   {attrs}")
        elif tag_num == gn.URI:
            print(f"  URI:       {content.decode('ascii')}")
        else:
            print(f"  [{tag_num}]:      {content.hex()}")

    # Certificates without a SAN extension return an empty list.
    no_san_cert = synta.Certificate.from_der(base64.b64decode(_RSA_CERT_B64))
    assert no_san_cert.subject_alt_names() == []
    print("  (cert without SAN → empty list: OK)")


def demo_parse_san():
    section("Parse SubjectAltName with peek_tag dispatch")
    cert = synta.Certificate.from_der(base64.b64decode(_SAN_CERT_B64))
    san_der = cert.get_extension_value_der(oids.SUBJECT_ALT_NAME)
    dec = synta.Decoder(san_der, synta.Encoding.DER)
    san_seq = dec.decode_sequence()

    _tag_names = {1: "rfc822Name", 2: "dNSName", 6: "uniformResourceIdentifier",
                  7: "iPAddress"}

    while not san_seq.is_empty():
        tag_no, tag_class, _ = san_seq.peek_tag()  # peek without consuming
        child = san_seq.decode_implicit_tag(tag_no, tag_class)
        raw = child.remaining_bytes()
        label = _tag_names.get(tag_no, f"[{tag_no}]")
        if tag_no == 7:       # iPAddress: 4 bytes for IPv4
            value = socket.inet_ntoa(raw)
        else:
            value = raw.decode("ascii")
        print(f"  {label}: {value}")


def demo_decode_raw_tlv():
    section("decode_raw_tlv — capture an extension as raw TLV bytes")
    cert = synta.Certificate.from_der(base64.b64decode(_RSA_CERT_B64))
    ext_der = cert.extensions_der
    dec = synta.Decoder(ext_der, synta.Encoding.DER)
    ext_seq = dec.decode_sequence()

    # Capture the first extension as a raw TLV blob
    raw_tlv = ext_seq.decode_raw_tlv()
    print(f"  First extension raw TLV: {len(raw_tlv)} bytes, "
          f"starts with tag 0x{raw_tlv[0]:02x}")
    # Re-parsing it yields the same extension
    dec2 = synta.Decoder(raw_tlv, synta.Encoding.DER)
    ext2 = dec2.decode_sequence()
    oid2 = ext2.decode_oid()
    print(f"  Re-parsed OID: {oid2}")


def demo_remaining_bytes():
    section("remaining_bytes — non-consuming read of value bytes")
    # Build [2] IMPLICIT IA5String "hello" (dNSName-style)
    name = b"hello"
    tlv = bytes([0x82, len(name)]) + name  # 0x82 = context[2] primitive
    dec = synta.Decoder(tlv, synta.Encoding.DER)
    child = dec.decode_implicit_tag(2, "Context")
    raw = child.remaining_bytes()
    print(f"  remaining_bytes(): {raw!r}")
    assert raw == name
    # Non-consuming: position unchanged
    assert child.remaining() == len(name)
    raw2 = child.remaining_bytes()
    assert raw2 == raw
    print("  Non-consuming: second call returns same bytes: OK")


def main():
    print("=" * 60)
    print("Example 3: Extension access and SAN parsing")
    print("=" * 60)
    demo_subject_alt_names()
    demo_extensions_der()
    demo_get_extension_value_der()
    demo_parse_basic_constraints()
    demo_parse_san()
    demo_decode_raw_tlv()
    demo_remaining_bytes()
    print("\nAll extension examples completed.")


if __name__ == "__main__":
    main()