# 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()
```