synta 0.1.11

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
# 8. `asn1parse.rs` — ASN.1 parser utility

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

Command-line tool that reads PEM or DER files and prints a structured
ASN.1 tree similar to `openssl asn1parse`.  Useful for debugging encoded
data.  Run as: `cargo run --example asn1parse <file>`.

## Source

```rust,ignore
//! ASN.1 Parser Utility
//!
//! This utility reads PEM or DER files and outputs an ASN.1 tree structure
//! similar to `openssl asn1parse`. It's useful for debugging and validating
//! ASN.1 encoded data.
//!
//! Usage:
//!   cargo run --example asn1parse <file>
//!   cargo run --example asn1parse <file> --offset <bytes>
//!
//! The output format matches `openssl asn1parse`:
//!   offset:d=depth hl=header_len l=content_len cons/prim: TYPE [value]
//!
//! Examples:
//!   cargo run --example asn1parse certificate.pem
//!   cargo run --example asn1parse certificate.der
//!   cargo run --example asn1parse data.der --offset 4

use std::env;
use std::fs;
use std::io::{self, Read};
use synta::{Decoder, Encoding, Tag, TagClass};

/// Parse DER or PEM file
fn read_asn1_file(path: &str) -> io::Result<Vec<u8>> {
    let mut file = fs::File::open(path)?;
    let mut contents = Vec::new();
    file.read_to_end(&mut contents)?;

    // Try to interpret as PEM first
    if contents.windows(11).any(|w| w == b"-----BEGIN ") {
        if let Some(der_data) = synta_certificate::pem_to_der(&contents).into_iter().next() {
            return Ok(der_data);
        }
    }

    // Otherwise assume it's DER
    Ok(contents)
}

/// Format tag name
fn tag_name(tag: &Tag) -> String {
    match tag.class() {
        TagClass::Universal => match tag.number() {
            1 => "BOOLEAN".to_string(),
            2 => "INTEGER".to_string(),
            3 => "BIT STRING".to_string(),
            4 => "OCTET STRING".to_string(),
            5 => "NULL".to_string(),
            6 => "OBJECT".to_string(),
            12 => "UTF8STRING".to_string(),
            16 => "SEQUENCE".to_string(),
            17 => "SET".to_string(),
            19 => "PrintableString".to_string(),
            22 => "IA5String".to_string(),
            23 => "UTCTIME".to_string(),
            24 => "GeneralizedTime".to_string(),
            n => format!("UNIVERSAL[{}]", n),
        },
        TagClass::Application => format!("appl [ {} ]", tag.number()),
        TagClass::ContextSpecific => format!("cont [ {} ]", tag.number()),
        TagClass::Private => format!("priv [ {} ]", tag.number()),
    }
}

/// Calculate tag encoding length
fn tag_length(tag: &Tag) -> usize {
    if tag.number() < 31 {
        1
    } else {
        // High-tag-number form: 1 byte + variable bytes
        let mut num = tag.number();
        let mut len = 1;
        while num > 0 {
            len += 1;
            num >>= 7;
        }
        len
    }
}

/// Calculate length encoding size
fn length_encoding_size(len: usize) -> usize {
    if len < 128 {
        1
    } else if len < 256 {
        2
    } else if len < 65536 {
        3
    } else if len < 16777216 {
        4
    } else {
        5
    }
}

/// Parse and print ASN.1 tree
fn parse_tree(data: &[u8], depth: usize, base_offset: usize) -> synta::Result<usize> {
    let mut decoder = Decoder::new(data, Encoding::Der);
    let start_pos = decoder.position();

    // Read tag
    let tag = decoder.read_tag()?;
    let tag_pos = start_pos;
    let tag_len = tag_length(&tag);

    // Read length
    let length = decoder.read_length()?;
    let content_len = length.definite()?;
    let length_len = length_encoding_size(content_len);

    let header_len = tag_len + length_len;
    let cons_prim = if tag.is_constructed() { "cons" } else { "prim" };
    let tag_name_str = tag_name(&tag);

    // Read content for primitive types
    let value_str = if !tag.is_constructed() && content_len > 0 {
        let content = decoder.read_bytes(content_len)?;

        // Format value based on type
        match tag.class() {
            TagClass::Universal => match tag.number() {
                2 => {
                    // INTEGER - show hex if small
                    if content.len() <= 8 {
                        format!(
                            ":{}",
                            content
                                .iter()
                                .map(|b| format!("{:02X}", b))
                                .collect::<String>()
                        )
                    } else {
                        String::new()
                    }
                }
                3 | 4 => {
                    // BIT STRING or OCTET STRING
                    if content.len() <= 8 {
                        format!(
                            ":{}",
                            content
                                .iter()
                                .map(|b| format!("{:02X}", b))
                                .collect::<String>()
                        )
                    } else {
                        String::new()
                    }
                }
                6 => {
                    // OBJECT IDENTIFIER - decode it
                    if let Ok(oid_str) = decode_oid(content) {
                        format!(":{}", oid_str)
                    } else {
                        String::new()
                    }
                }
                12 | 19 | 22 => {
                    // String types
                    if let Ok(s) = std::str::from_utf8(content) {
                        format!(":{}", s)
                    } else {
                        String::new()
                    }
                }
                23 | 24 => {
                    // Time types
                    if let Ok(s) = std::str::from_utf8(content) {
                        format!(":{}", s)
                    } else {
                        String::new()
                    }
                }
                _ => String::new(),
            },
            _ => String::new(),
        }
    } else {
        String::new()
    };

    println!(
        "{:5}:d={:2} hl={} l={:4} {}: {}{}",
        base_offset + tag_pos,
        depth,
        header_len,
        content_len,
        cons_prim,
        tag_name_str,
        value_str
    );

    // Recursively parse constructed types
    if tag.is_constructed() && content_len > 0 {
        let content_start = decoder.position();
        let content_data = &data[content_start..content_start + content_len];

        let mut parsed = 0;
        while parsed < content_len {
            let child_len = parse_tree(
                &content_data[parsed..],
                depth + 1,
                base_offset + content_start + parsed,
            )?;
            parsed += child_len;
        }
    }

    Ok(header_len + content_len)
}

/// Decode OID from bytes
fn decode_oid(data: &[u8]) -> Result<String, ()> {
    if data.is_empty() {
        return Err(());
    }

    let mut components = Vec::new();

    // First byte encodes first two components
    let first = data[0];
    components.push((first / 40) as u32);
    components.push((first % 40) as u32);

    // Remaining bytes encode subsequent components
    let mut i = 1;
    while i < data.len() {
        let mut value = 0u32;
        loop {
            if i >= data.len() {
                break;
            }
            let byte = data[i];
            i += 1;

            value = (value << 7) | (byte & 0x7F) as u32;

            if byte & 0x80 == 0 {
                break;
            }
        }
        components.push(value);
    }

    Ok(components
        .iter()
        .map(|c| c.to_string())
        .collect::<Vec<_>>()
        .join("."))
}

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        eprintln!("Usage: {} <file> [--offset <bytes>]", args[0]);
        eprintln!();
        eprintln!("Parse ASN.1 DER or PEM file and display structure.");
        eprintln!();
        eprintln!("Examples:");
        eprintln!("  {} certificate.pem", args[0]);
        eprintln!("  {} certificate.der", args[0]);
        eprintln!("  {} data.der --offset 4", args[0]);
        std::process::exit(1);
    }

    let file_path = &args[1];

    // Parse optional offset
    let start_offset = if args.len() >= 4 && args[2] == "--offset" {
        args[3].parse::<usize>().unwrap_or(0)
    } else {
        0
    };

    // Read file
    let data = match read_asn1_file(file_path) {
        Ok(data) => data,
        Err(e) => {
            eprintln!("Error reading file: {}", e);
            std::process::exit(1);
        }
    };

    // Apply offset if specified
    let parse_data = if start_offset > 0 {
        if start_offset >= data.len() {
            eprintln!("Offset {} exceeds file size {}", start_offset, data.len());
            std::process::exit(1);
        }
        &data[start_offset..]
    } else {
        &data
    };

    // Parse ASN.1 tree
    match parse_tree(parse_data, 0, start_offset) {
        Ok(_) => {}
        Err(e) => {
            eprintln!("Error parsing ASN.1: {:?}", e);
            std::process::exit(1);
        }
    }
}
```