# 12. `name_constraints.rs` — NameConstraints extension (RFC 5280 §4.2.1.10)
[← Example index](index.md) · [name_constraints.rs on Codeberg](https://codeberg.org/abbra/synta/src/branch/main/examples/name_constraints.rs)
Demonstrates `NameConstraintsBuilder` from `synta_certificate`:
- Build a `NameConstraints` DER value with `permit_dns`, `exclude_dns`,
`permit_ip`, `exclude_ip`, and `permit_rfc822`.
- Verify the raw DER output — confirm the `[0] IMPLICIT GeneralSubtrees`
(permitted) and `[1] IMPLICIT GeneralSubtrees` (excluded) CONSTRUCTED
context tags using a simple byte-level TLV walker.
- Show that `permit_*`-only and `exclude_*`-only calls produce DER with
only the relevant IMPLICIT field present.
- Combine all constraint types in a single builder chain.
## Source
```rust,ignore
//! Example: NameConstraints X.509 extension (RFC 5280 §4.2.1.10)
//!
//! Demonstrates `NameConstraintsBuilder` from `synta_certificate`:
//!
//! - Building a `NameConstraints` DER value with DNS, IP, and RFC 822
//! (email) constraints in both the permitted and excluded subtrees.
//! - Verifying the resulting DER structure by walking the raw bytes to
//! confirm the `[0] IMPLICIT GeneralSubtrees` (permitted) and
//! `[1] IMPLICIT GeneralSubtrees` (excluded) CONSTRUCTED context tags.
//! - Showing the `permit_*` / `exclude_*` fluent API: `permit_dns`,
//! `exclude_dns`, `permit_ip`, `exclude_ip`, `permit_rfc822`.
//!
//! Run with:
//! cargo run --example name_constraints
use synta_certificate::NameConstraintsBuilder;
fn section(title: &str) {
println!("\n{}\n{title}\n{}", "─".repeat(60), "─".repeat(60));
}
// ── DER helpers ───────────────────────────────────────────────────────────────
/// Classify the tag byte into a human-readable class string and return the
/// tag number and constructed bit.
fn classify_tag(tag_byte: u8) -> (&'static str, u8, bool) {
let class = match (tag_byte >> 6) & 0x3 {
0 => "Universal",
1 => "Application",
2 => "Context",
3 => "Private",
_ => unreachable!(),
};
let is_constructed = (tag_byte & 0x20) != 0;
let tag_number = tag_byte & 0x1f;
(class, tag_number, is_constructed)
}
/// Parse the DER length field at `data[0..]`.
/// Returns `(length_value, header_bytes_consumed)`.
fn parse_der_len(data: &[u8]) -> (usize, usize) {
assert!(!data.is_empty(), "truncated DER length");
let first = data[0];
if first < 0x80 {
(first as usize, 1)
} else {
let n = (first & 0x7f) as usize;
assert!(data.len() > n, "truncated multi-byte DER length");
let mut len = 0usize;
for &b in &data[1..=n] {
len = (len << 8) | b as usize;
}
(len, 1 + n)
}
}
/// Strip the outer SEQUENCE (0x30) tag+length wrapper and return the
/// content slice.
fn unwrap_sequence(der: &[u8]) -> &[u8] {
assert_eq!(der[0], 0x30, "expected SEQUENCE tag 0x30");
let (len, hdr) = parse_der_len(&der[1..]);
assert_eq!(1 + hdr + len, der.len(), "SEQUENCE length mismatch");
&der[1 + hdr..]
}
/// Read one TLV record from the front of `data`.
/// Returns `(tag_byte, value_slice, bytes_consumed)`.
fn read_tlv(data: &[u8]) -> (u8, &[u8], usize) {
assert!(!data.is_empty(), "expected TLV, got empty slice");
let tag_byte = data[0];
let (len, hdr) = parse_der_len(&data[1..]);
let value = &data[1 + hdr..1 + hdr + len];
(tag_byte, value, 1 + hdr + len)
}
// ── Demo functions ────────────────────────────────────────────────────────────
/// Build a `NameConstraints` value with DNS-only constraints and verify
/// that the DER contains exactly one `[0] IMPLICIT` and one `[1] IMPLICIT`
/// field.
fn demo_dns_subtrees() {
section("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 that
// exact host name.
let nc_der = NameConstraintsBuilder::new()
.permit_dns(".example.com") // any subdomain of example.com
.permit_dns("exact.example.net") // only the exact host name
.exclude_dns(".evil.example.com") // block the entire evil subtree
.build()
.expect("DNS NameConstraints DER encoding failed");
println!(" NC DER (DNS only): {} bytes, tag=0x{:02x}", nc_der.len(), nc_der[0]);
// Walk the outer SEQUENCE content.
let content = unwrap_sequence(&nc_der);
// permittedSubtrees [0] IMPLICIT CONSTRUCTED — tag byte 0xa0
let (tag_byte, value, consumed) = read_tlv(content);
let (class, tag_no, is_constr) = classify_tag(tag_byte);
assert_eq!(class, "Context", "expected Context class for [0]");
assert_eq!(tag_no, 0, "expected tag number 0 for permittedSubtrees");
assert!(is_constr, "[0] must be CONSTRUCTED");
println!(" permittedSubtrees [0]: {} content bytes", value.len());
// excludedSubtrees [1] IMPLICIT CONSTRUCTED — tag byte 0xa1
let (tag_byte, value, _) = read_tlv(&content[consumed..]);
let (class, tag_no, is_constr) = classify_tag(tag_byte);
assert_eq!(class, "Context", "expected Context class for [1]");
assert_eq!(tag_no, 1, "expected tag number 1 for excludedSubtrees");
assert!(is_constr, "[1] must be CONSTRUCTED");
println!(" excludedSubtrees [1]: {} content bytes", value.len());
println!(" DNS subtree structure: OK");
}
/// Build a `NameConstraints` value with IPv4 address-range constraints
/// and RFC 822 (email) domain constraints.
fn demo_ip_and_email_subtrees() {
section("IP prefix and RFC 822 (email) constraints");
// IPv4 IP constraint format: 4-byte address || 4-byte mask (8 bytes total).
// Permit 192.168.0.0/16: address=192.168.0.0, mask=255.255.0.0
let ipv4_permit: &[u8] = &[192, 168, 0, 0, 255, 255, 0, 0];
// Exclude 10.0.0.0/8: address=10.0.0.0, mask=255.0.0.0
let ipv4_exclude: &[u8] = &[10, 0, 0, 0, 255, 0, 0, 0];
let nc_der = NameConstraintsBuilder::new()
.permit_ip(ipv4_permit)
.permit_rfc822("example.com") // all @example.com mailboxes
.exclude_ip(ipv4_exclude)
.exclude_rfc822("marketing.example.com") // block @marketing.example.com
.build()
.expect("IP + RFC 822 NameConstraints DER encoding failed");
println!(" NC DER (IP + RFC 822): {} bytes, tag=0x{:02x}", nc_der.len(), nc_der[0]);
assert_eq!(nc_der[0], 0x30, "expected SEQUENCE tag");
let content = unwrap_sequence(&nc_der);
// Both [0] and [1] must be present.
let (tag_byte, _, consumed) = read_tlv(content);
let (_, tag_no, _) = classify_tag(tag_byte);
assert_eq!(tag_no, 0, "expected permittedSubtrees [0] first");
let (tag_byte, _, _) = read_tlv(&content[consumed..]);
let (_, tag_no, _) = classify_tag(tag_byte);
assert_eq!(tag_no, 1, "expected excludedSubtrees [1] second");
println!(" IP prefix + RFC 822 structure: OK");
}
/// Show that when only `permit_*` methods are called the DER contains only the
/// `[0] IMPLICIT` field with no trailing `[1]` field.
fn demo_permitted_only() {
section("Permitted subtrees only (no exclusions)");
let nc_der = NameConstraintsBuilder::new()
.permit_dns(".acme.example")
.permit_rfc822("acme.example")
.build()
.expect("permit-only NameConstraints DER encoding failed");
println!(" NC DER (permitted only): {} bytes", nc_der.len());
assert_eq!(nc_der[0], 0x30);
let content = unwrap_sequence(&nc_der);
let (tag_byte, value, consumed) = read_tlv(content);
let (_, tag_no, _) = classify_tag(tag_byte);
assert_eq!(tag_no, 0, "expected only [0] (permitted)");
println!(" permittedSubtrees [0]: {} content bytes", value.len());
// No further TLV records — excludedSubtrees must be absent.
assert!(
content[consumed..].is_empty(),
"expected no excludedSubtrees field when none were added"
);
println!(" Only permittedSubtrees present, excludedSubtrees absent: OK");
}
/// Show that when only `exclude_*` methods are called the DER contains only
/// the `[1] IMPLICIT` field with no preceding `[0]` field.
fn demo_excluded_only() {
section("Excluded subtrees only (no permitted)");
let nc_der = NameConstraintsBuilder::new()
.exclude_dns(".blocked.example")
.build()
.expect("exclude-only NameConstraints DER encoding failed");
println!(" NC DER (excluded only): {} bytes", nc_der.len());
assert_eq!(nc_der[0], 0x30);
let content = unwrap_sequence(&nc_der);
let (tag_byte, value, consumed) = read_tlv(content);
let (_, tag_no, _) = classify_tag(tag_byte);
assert_eq!(tag_no, 1, "expected only [1] (excluded) when no permitted entries");
println!(" excludedSubtrees [1]: {} content bytes", value.len());
assert!(
content[consumed..].is_empty(),
"expected no permittedSubtrees field when none were added"
);
println!(" Only excludedSubtrees present, permittedSubtrees absent: OK");
}
/// Combine all constraint types in a single builder call, demonstrating the
/// full range of `NameConstraintsBuilder` methods.
fn demo_combined_constraints() {
section("Combined DNS, IP, and RFC 822 constraints");
let nc_der = NameConstraintsBuilder::new()
// Permitted subtrees
.permit_dns(".example.com")
.permit_rfc822(".example.com")
.permit_ip(&[192, 168, 0, 0, 255, 255, 0, 0]) // 192.168.0.0/16
// Excluded subtrees
.exclude_dns(".legacy.example.com")
.exclude_rfc822("postmaster@example.com") // exact mailbox excluded
.exclude_ip(&[10, 0, 0, 0, 255, 0, 0, 0]) // 10.0.0.0/8
.build()
.expect("combined NameConstraints DER encoding failed");
println!(" NC DER (combined): {} bytes, tag=0x{:02x}", nc_der.len(), nc_der[0]);
assert_eq!(nc_der[0], 0x30, "expected SEQUENCE tag");
let content = unwrap_sequence(&nc_der);
let (_, perm_value, perm_consumed) = read_tlv(content);
let (_, excl_value, _) = read_tlv(&content[perm_consumed..]);
println!(" permittedSubtrees content: {} bytes", perm_value.len());
println!(" excludedSubtrees content: {} bytes", excl_value.len());
println!(" Combined constraint round-trip: OK");
}
// ── Entry point ───────────────────────────────────────────────────────────────
fn main() {
println!("=== NameConstraints (RFC 5280 §4.2.1.10) example ===");
demo_dns_subtrees();
demo_ip_and_email_subtrees();
demo_permitted_only();
demo_excluded_only();
demo_combined_constraints();
println!("\nAll steps completed successfully.");
}
```