synta 0.1.5

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
# 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.");
}
```