synta 0.1.12

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
# 32. `example_name_constraints.py` — NameConstraints extension (RFC 5280 §4.2.1.10)

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

Bindings: `synta.ext.NameConstraintsBuilder` (alias `NC`),
`synta.oids.NAME_CONSTRAINTS`, `Certificate.get_extension_value_der`,
`CertificateBuilder.add_extension`, `PrivateKey.generate_ec`, `NameBuilder`.

- Build a `NameConstraints` value with `permit_dns` (with and without a leading dot),
  `exclude_dns`, `permit_ip` (8-byte IPv4 address+mask), `exclude_ip`,
  and `permit_rfc822` using the `NC` alias.
- Verify the DER structure: decode the outer SEQUENCE and confirm the
  `[0] IMPLICIT` (permittedSubtrees) and `[1] IMPLICIT` (excludedSubtrees)
  CONSTRUCTED context tags via `Decoder`.
- Show that `permit_*`-only and `exclude_*`-only builders produce DER with
  only the relevant IMPLICIT field present.
- Build a self-signed CA certificate with `NameConstraints` marked critical
  (as required by RFC 5280 §4.2.1.10); look it up with
  `get_extension_value_der(oids.NAME_CONSTRAINTS)`; walk `extensions_der`
  to confirm the critical flag is set; verify DER round-trip.
- Confirm that `NC` (short alias) and `NameConstraintsBuilder` (full class
  name) produce identical DER.

## Source

```python
#!/usr/bin/env python3
"""
Example: NameConstraints X.509 Extension (RFC 5280 §4.2.1.10)

Demonstrates:
  - Building NameConstraints DER with NameConstraintsBuilder (alias: NC)
  - permit_dns, exclude_dns — DNS subtree constraints with and without leading dot
  - permit_ip, exclude_ip — IPv4 address+mask constraints (8 bytes each)
  - permit_rfc822 — RFC 822 (email) domain constraints
  - Building a self-signed CA certificate that carries NameConstraints critical
  - Parsing the resulting DER with Certificate.from_der and reading back
    the extension value via get_extension_value_der
  - Round-tripping the NameConstraints DER through the Decoder to verify
    the [0] IMPLICIT and [1] IMPLICIT structure

Run with:
    PYTHONPATH=python python3 examples/example_name_constraints.py
"""

import datetime

import synta
import synta.ext as ext
import synta.oids as oids


# ── Shared time constants ─────────────────────────────────────────────────────

_UTC = datetime.timezone.utc
_NOW = datetime.datetime(2026, 1, 1, tzinfo=_UTC)
_TWO_YEARS = datetime.datetime(2028, 1, 1, tzinfo=_UTC)


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


# ── Demo functions ────────────────────────────────────────────────────────────

def demo_builder_dns_only() -> None:
    section("NameConstraintsBuilder — DNS permitted and excluded subtrees")

    # A leading dot (e.g. ".example.com") constrains all subdomains of
    # example.com.  Without the dot the constraint applies only to the
    # exact host name.
    nc_der = (
        ext.NC()
        .permit_dns(".example.com")      # subtree: any subdomain of example.com
        .permit_dns("exact.example.net") # only the exact host name
        .exclude_dns(".evil.example.com") # forbid the evil subdomain entirely
        .build()
    )

    print(f"  NC DER (DNS only): {len(nc_der)} bytes, tag=0x{nc_der[0]:02x}")
    # Outer SEQUENCE tag must be 0x30
    assert nc_der[0] == 0x30, f"expected SEQUENCE tag, got 0x{nc_der[0]:02x}"

    # Decode the outer SEQUENCE and verify the [0] IMPLICIT / [1] IMPLICIT tags.
    dec = synta.Decoder(nc_der, synta.Encoding.DER)
    outer = dec.decode_sequence()               # NameConstraints SEQUENCE

    # permittedSubtrees [0] IMPLICIT — constructed context tag 0xa0
    tag_no, tag_class, is_constr = outer.peek_tag()
    assert tag_no == 0 and tag_class == "Context" and is_constr, (
        f"expected [0] CONSTRUCTED CONTEXT, got tag={tag_no} class={tag_class}"
    )
    permitted_field = outer.decode_implicit_tag(0, "Context")
    permitted_bytes = permitted_field.remaining_bytes()
    print(f"  permittedSubtrees [0]: {len(permitted_bytes)} content bytes")

    # excludedSubtrees [1] IMPLICIT — constructed context tag 0xa1
    tag_no, tag_class, is_constr = outer.peek_tag()
    assert tag_no == 1 and tag_class == "Context" and is_constr, (
        f"expected [1] CONSTRUCTED CONTEXT, got tag={tag_no} class={tag_class}"
    )
    excluded_field = outer.decode_implicit_tag(1, "Context")
    excluded_bytes = excluded_field.remaining_bytes()
    print(f"  excludedSubtrees [1]: {len(excluded_bytes)} content bytes")

    print("  DNS subtree structure: OK")


def demo_builder_ip_and_email() -> None:
    section("NameConstraintsBuilder — IP prefix and RFC 822 (email) constraints")

    # IPv4 IP constraint format: 4-byte address followed by 4-byte mask (8 bytes total).
    # 192.168.0.0/16: address = 192.168.0.0, mask = 255.255.0.0
    ipv4_permit = bytes([192, 168, 0, 0, 255, 255, 0, 0])

    # 10.0.0.0/8: address = 10.0.0.0, mask = 255.0.0.0  — exclude
    ipv4_exclude = bytes([10, 0, 0, 0, 255, 0, 0, 0])

    # RFC 822 (email) constraint: bare domain permits all @example.com addresses.
    nc_der = (
        ext.NC()
        .permit_ip(ipv4_permit)
        .permit_rfc822("example.com")           # all @example.com mailboxes
        .exclude_ip(ipv4_exclude)
        .exclude_rfc822("marketing.example.com") # no @marketing.example.com
        .build()
    )

    print(f"  NC DER (IP + RFC 822): {len(nc_der)} bytes, tag=0x{nc_der[0]:02x}")
    assert nc_der[0] == 0x30, "expected SEQUENCE tag"

    # Both permittedSubtrees and excludedSubtrees must be present.
    dec = synta.Decoder(nc_der, synta.Encoding.DER)
    outer = dec.decode_sequence()
    tag_no, tag_class, _ = outer.peek_tag()
    assert tag_no == 0 and tag_class == "Context", "expected permittedSubtrees [0]"
    outer.decode_implicit_tag(0, "Context")     # consume permitted subtrees

    tag_no, tag_class, _ = outer.peek_tag()
    assert tag_no == 1 and tag_class == "Context", "expected excludedSubtrees [1]"
    outer.decode_implicit_tag(1, "Context")     # consume excluded subtrees

    assert outer.is_empty(), "unexpected trailing bytes in NameConstraints SEQUENCE"
    print("  IP prefix + RFC 822 structure: OK")


def demo_permitted_only() -> None:
    section("NameConstraintsBuilder — permitted subtrees only (no exclusions)")

    nc_der = (
        ext.NC()
        .permit_dns(".acme.example")
        .permit_rfc822("acme.example")
        .build()
    )

    print(f"  NC DER (permitted only): {len(nc_der)} bytes")
    assert nc_der[0] == 0x30

    dec = synta.Decoder(nc_der, synta.Encoding.DER)
    outer = dec.decode_sequence()

    # Only the permitted ([0]) field should be present; no excluded ([1]) field.
    tag_no, tag_class, _ = outer.peek_tag()
    assert tag_no == 0 and tag_class == "Context", "expected only [0] (permitted)"
    outer.decode_implicit_tag(0, "Context")
    assert outer.is_empty(), "expected no excludedSubtrees when none were added"
    print("  Only permittedSubtrees present, excludedSubtrees absent: OK")


def demo_excluded_only() -> None:
    section("NameConstraintsBuilder — excluded subtrees only (no permitted)")

    nc_der = (
        ext.NC()
        .exclude_dns(".blocked.example")
        .build()
    )

    print(f"  NC DER (excluded only): {len(nc_der)} bytes")
    assert nc_der[0] == 0x30

    dec = synta.Decoder(nc_der, synta.Encoding.DER)
    outer = dec.decode_sequence()

    # Only the excluded ([1]) field should be present; [0] is absent.
    tag_no, tag_class, _ = outer.peek_tag()
    assert tag_no == 1 and tag_class == "Context", "expected only [1] (excluded)"
    outer.decode_implicit_tag(1, "Context")
    assert outer.is_empty(), "expected no permittedSubtrees when none were added"
    print("  Only excludedSubtrees present, permittedSubtrees absent: OK")


def demo_ca_certificate_with_name_constraints() -> None:
    section("CA certificate with NameConstraints critical extension")

    # Generate a fresh EC P-256 key pair for the CA.
    ca_key = synta.PrivateKey.generate_ec("P-256")

    # Build the CA subject and issuer Name.
    ca_name = (
        synta.NameBuilder()
        .country("US")
        .organization("Example Corp")
        .organizational_unit("PKI")
        .common_name("Constrained Intermediate CA")
        .build()
    )

    # Build the NameConstraints extension value.
    # This CA may only issue certificates for:
    #   - DNS names inside example.com
    #   - Email addresses in example.com
    #   - Hosts in 192.168.0.0/16
    # It must NOT issue for:
    #   - The legacy subdomain legacy.example.com
    #   - The 10.0.0.0/8 address block
    nc_der = (
        ext.NC()
        .permit_dns(".example.com")
        .permit_rfc822(".example.com")
        .permit_ip(bytes([192, 168, 0, 0, 255, 255, 0, 0]))  # 192.168.0.0/16
        .exclude_dns(".legacy.example.com")
        .exclude_ip(bytes([10, 0, 0, 0, 255, 0, 0, 0]))      # 10.0.0.0/8
        .build()
    )
    print(f"  NameConstraints DER: {len(nc_der)} bytes")

    # Standard CA extensions.
    bc_der = ext.basic_constraints(ca=True, path_length=0)
    ku_der = ext.key_usage(ext.KU_KEY_CERT_SIGN | ext.KU_CRL_SIGN)
    spki_der = ca_key.public_key.to_der()
    ski_der = ext.subject_key_identifier(spki_der)

    # Build and sign the CA certificate.
    ca_cert = (
        synta.CertificateBuilder()
        .issuer_name(ca_name)
        .subject_name(ca_name)
        .public_key(ca_key.public_key)
        .serial_number(1)
        .not_valid_before_utc(_NOW)
        .not_valid_after_utc(_TWO_YEARS)
        .add_extension("2.5.29.19", True, bc_der)    # basicConstraints critical
        .add_extension("2.5.29.15", True, ku_der)    # keyUsage critical
        .add_extension("2.5.29.14", False, ski_der)  # subjectKeyIdentifier
        # RFC 5280 §4.2.1.10: NameConstraints MUST be critical in a CA certificate.
        .add_extension("2.5.29.30", True, nc_der)    # nameConstraints critical
        .sign(ca_key, "sha256")
    )

    assert isinstance(ca_cert, synta.Certificate)
    print(f"  CA cert issuer:  {ca_cert.issuer}")
    print(f"  CA cert subject: {ca_cert.subject}")
    print(f"  CA cert serial:  {ca_cert.serial_number}")
    print(f"  CA cert DER:     {len(ca_cert.to_der())} bytes")

    # Look up the NameConstraints extension by its OID.
    nc_back = ca_cert.get_extension_value_der(oids.NAME_CONSTRAINTS)
    assert nc_back is not None, "NameConstraints extension not found in parsed cert"
    # The retrieved extnValue bytes must be identical to what we encoded.
    assert nc_back == nc_der, (
        f"round-tripped NC DER differs: {nc_back.hex()} != {nc_der.hex()}"
    )
    print(f"  get_extension_value_der(NAME_CONSTRAINTS): {len(nc_back)} bytes  OK")

    # Verify the extension is critical by walking extensions_der manually.
    ext_der = ca_cert.extensions_der
    assert ext_der is not None
    dec = synta.Decoder(ext_der, synta.Encoding.DER)
    ext_seq = dec.decode_sequence()
    nc_oid_str = str(oids.NAME_CONSTRAINTS)
    found_critical = False
    while not ext_seq.is_empty():
        one = ext_seq.decode_sequence()
        oid_val = one.decode_oid()
        if str(oid_val) == nc_oid_str:
            # The next field should be a BOOLEAN TRUE (critical flag).
            tag_no, tag_class, _ = one.peek_tag()
            if tag_no == 1 and tag_class == "Universal":
                critical = one.decode_boolean().value()
                found_critical = critical
            break
    assert found_critical, "NameConstraints extension was not marked critical"
    print("  NameConstraints marked critical: OK")

    # DER round-trip.
    ca_cert2 = synta.Certificate.from_der(ca_cert.to_der())
    assert ca_cert2.issuer == ca_cert.issuer
    assert ca_cert2.serial_number == ca_cert.serial_number
    print("  DER round-trip: OK")


def demo_short_alias() -> None:
    section("Short alias NC vs full class name NameConstraintsBuilder")

    nc1 = (
        ext.NC()
        .permit_dns(".example.com")
        .exclude_dns(".bad.example.com")
        .build()
    )

    nc2 = (
        ext.NameConstraintsBuilder()
        .permit_dns(".example.com")
        .exclude_dns(".bad.example.com")
        .build()
    )

    # NC and NameConstraintsBuilder are the same class; both chains produce
    # identical DER.
    assert nc1 == nc2, "NC alias produces different DER than NameConstraintsBuilder"
    print(f"  NC alias == NameConstraintsBuilder: {nc1.hex()[:20]}...  OK")


def main() -> None:
    print("=== NameConstraints Extension (RFC 5280 §4.2.1.10) example ===")

    demo_builder_dns_only()
    demo_builder_ip_and_email()
    demo_permitted_only()
    demo_excluded_only()
    demo_ca_certificate_with_name_constraints()
    demo_short_alias()

    print("\nAll steps completed successfully.")


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