refactor(parser): move parsing modules into parser/ submodule; feat(x509): detect Certificate and label version/serial; fix: clippy warnings and context labels

This commit is contained in:
Christopher Williams 2025-08-21 10:54:58 -04:00
parent e18612fd4a
commit 3bf914ee15
9 changed files with 171 additions and 45 deletions

View File

@ -2,15 +2,6 @@ mod cli;
mod color;
mod parser;
// New modules for the refactored parser
mod asn1_types;
mod certificate_utils;
mod encoding_utils;
mod oid_registry;
mod pretty_printer;
mod time_utils;
mod value_printer;
use clap::Parser as _;
use std::fs;
use std::io::{self, Read};
@ -71,7 +62,7 @@ fn main() {
// Read raw binary data from stdin
let mut buf = Vec::new();
if let Err(e) = io::stdin().read_to_end(&mut buf) {
eprintln!("Failed to read from stdin: {}", e);
eprintln!("Failed to read from stdin: {e}");
std::process::exit(2);
}
if buf.is_empty() {
@ -84,7 +75,7 @@ fn main() {
// Read text and parse as hex
let mut buf = String::new();
if let Err(e) = io::stdin().read_to_string(&mut buf) {
eprintln!("Failed to read from stdin: {}", e);
eprintln!("Failed to read from stdin: {e}");
std::process::exit(2);
}
if buf.trim().is_empty() {
@ -98,7 +89,7 @@ fn main() {
// Try to read as text first (for hex), fall back to binary
let mut buf = Vec::new();
if let Err(e) = io::stdin().read_to_end(&mut buf) {
eprintln!("Failed to read from stdin: {}", e);
eprintln!("Failed to read from stdin: {e}");
std::process::exit(2);
}
if buf.is_empty() {

View File

@ -17,12 +17,16 @@ impl TagClass {
}
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ParentCtx {
Root,
Sequence,
Set,
Ctx,
// X.509-specific contexts (best-effort detection)
Certificate,
TbsCertificate,
TbsCertVersion,
}
#[derive(Clone, Copy)]
@ -220,3 +224,4 @@ mod tests {
assert_eq!(ip, "192.168.1.1");
}
}

View File

@ -1,4 +1,4 @@
use crate::asn1_types::{parse_tlv, TagClass};
use super::asn1_types::{parse_tlv, TagClass};
pub fn decode_keyusage_from_octet(octet: &[u8]) -> Option<Vec<&'static str>> {
let tlv = parse_tlv(octet)?;
@ -66,3 +66,39 @@ pub fn try_decode_inner_seq(bytes: &[u8], color: bool) -> Option<String> {
Err(_) => None,
}
}
/// Best-effort shape check for an X.509 Certificate at the given bytes.
///
/// We look for:
/// - Outer SEQUENCE
/// - Content contains: SEQUENCE (tbsCertificate), SEQUENCE (algorithmIdentifier starting with OID), BIT STRING (signatureValue)
pub fn looks_like_x509_certificate(bytes: &[u8]) -> bool {
// Outer SEQUENCE
let Some(outer) = parse_tlv(bytes) else { return false; };
if outer.class != TagClass::Universal || outer.tag != 16 || !outer.constructed {
return false;
}
let content = &bytes[outer.content_start..outer.content_end()];
// First child: tbsCertificate SEQUENCE
let Some(tbs) = parse_tlv(content) else { return false; };
if tbs.class != TagClass::Universal || tbs.tag != 16 || !tbs.constructed { return false; }
// Second child: algorithmIdentifier SEQUENCE whose first child is an OID
let off_alg = tbs.total_len();
if off_alg >= content.len() { return false; }
let Some(alg) = parse_tlv(&content[off_alg..]) else { return false; };
if alg.class != TagClass::Universal || alg.tag != 16 || !alg.constructed { return false; }
// Inside algorithmIdentifier: first element OID
let alg_content = &content[off_alg + alg.content_start..off_alg + alg.content_end()];
let Some(alg_oid) = parse_tlv(alg_content) else { return false; };
if alg_oid.class != TagClass::Universal || alg_oid.tag != 6 { return false; }
// Third child: signatureValue BIT STRING
let off_sig = off_alg + alg.total_len();
if off_sig >= content.len() { return false; }
let Some(sig) = parse_tlv(&content[off_sig..]) else { return false; };
if sig.class != TagClass::Universal || sig.tag != 3 || sig.constructed { return false; }
true
}

View File

@ -206,3 +206,4 @@ mod tests {
assert!(truncated);
}
}

View File

@ -1,9 +1,22 @@
// Re-export commonly used types and functions
pub use crate::asn1_types::ParentCtx;
pub use crate::encoding_utils::{hex_to_bytes, BitsDisplay, BitsFormat};
pub use crate::pretty_printer::pretty_print_der;
pub mod asn1_types;
pub mod certificate_utils;
pub mod encoding_utils;
pub mod oid_registry;
pub mod pretty_printer;
pub mod time_utils;
pub mod value_printer;
pub fn parse_and_print(bytes: &[u8], color: bool, recursive: bool, bits_disp: BitsDisplay) -> Result<(), yasna::ASN1Error> {
// Re-export commonly used types and functions
pub use self::asn1_types::ParentCtx;
pub use self::encoding_utils::{hex_to_bytes, BitsDisplay, BitsFormat};
pub use self::pretty_printer::pretty_print_der;
pub fn parse_and_print(
bytes: &[u8],
color: bool,
recursive: bool,
bits_disp: BitsDisplay,
) -> Result<(), yasna::ASN1Error> {
// Generic pretty-printer for any DER (with color)
pretty_print_der(bytes, color, recursive, 0, ParentCtx::Root, bits_disp);
Ok(())

View File

@ -64,3 +64,4 @@ pub fn oid_friendly(oid: &str) -> Option<&'static str> {
_ => None,
}
}

View File

@ -1,7 +1,8 @@
use crate::asn1_types::{decode_oid, parse_tlv, ParentCtx, TagClass, TlvInfo};
use super::asn1_types::{decode_oid, parse_tlv, ParentCtx, TagClass, TlvInfo};
use crate::color::{paint, BOLD, MAGENTA};
use crate::encoding_utils::BitsDisplay;
use crate::value_printer::{print_indent, print_primitive_value_with_label, PrintCtx};
use super::encoding_utils::BitsDisplay;
use super::value_printer::{print_indent, print_primitive_value_with_label, PrintCtx};
use super::certificate_utils::looks_like_x509_certificate;
pub fn pretty_print_der(
input: &[u8],
@ -22,7 +23,43 @@ pub fn pretty_print_der(
let content = &base[tlv.content_start..tlv.content_end()];
if should_print_as_constructed(&tlv) {
print_constructed_value(&tlv, content, color, recursive, indent, bits_disp);
// Special handling to propagate X.509-aware contexts
if tlv.class == TagClass::Universal && tlv.tag == 16 {
// SEQUENCE cases
if indent == 0 && parent_ctx == ParentCtx::Root && looks_like_x509_certificate(base) {
// Outer Certificate
print_indent(indent);
println!("{} {{", paint("SEQUENCE", BOLD, color));
pretty_print_der(content, color, recursive, indent + 1, ParentCtx::Certificate, bits_disp);
print_indent(indent);
println!("}}");
} else if parent_ctx == ParentCtx::Certificate && child_index == 0 {
// tbsCertificate
print_indent(indent);
println!("{} {{", paint("SEQUENCE", BOLD, color));
pretty_print_der(content, color, recursive, indent + 1, ParentCtx::TbsCertificate, bits_disp);
print_indent(indent);
println!("}}");
} else {
print_constructed_value(&tlv, content, color, recursive, indent, parent_ctx, bits_disp);
}
} else if tlv.class == TagClass::ContextSpecific && tlv.constructed && parent_ctx == ParentCtx::TbsCertificate && tlv.tag == 0 {
// [0] EXPLICIT Version inside TBSCertificate
print_indent(indent);
println!("[CTX {}] version {{", tlv.tag);
pretty_print_der(content, color, recursive, indent + 1, ParentCtx::TbsCertVersion, bits_disp);
print_indent(indent);
println!("}}");
} else if tlv.class == TagClass::ContextSpecific && tlv.constructed && parent_ctx == ParentCtx::TbsCertificate && tlv.tag == 3 {
// [3] EXPLICIT Extensions inside TBSCertificate
print_indent(indent);
println!("[CTX {}] extensions {{", tlv.tag);
pretty_print_der(content, color, recursive, indent + 1, ParentCtx::Ctx, bits_disp);
print_indent(indent);
println!("}}");
} else {
print_constructed_value(&tlv, content, color, recursive, indent, parent_ctx, bits_disp);
}
} else {
let label = determine_label(&tlv, parent_ctx, child_index, &last_child);
let ctx = PrintCtx {
@ -60,6 +97,7 @@ fn print_constructed_value(
color: bool,
recursive: bool,
indent: usize,
_outer_parent_ctx: ParentCtx,
bits_disp: BitsDisplay,
) {
print_indent(indent);
@ -69,6 +107,8 @@ fn print_constructed_value(
(TagClass::Universal, 17) => ("SET", ParentCtx::Set),
(TagClass::ContextSpecific, tag) => {
println!("[CTX {tag}] {{");
// Default: context-specific constructed elements recurse into a generic context
// (callers can intercept special cases before reaching here)
pretty_print_der(content, color, recursive, indent + 1, ParentCtx::Ctx, bits_disp);
print_indent(indent);
println!("}}");
@ -90,24 +130,47 @@ fn determine_label<'a>(
last_child: &Option<(TagClass, bool, u64, Vec<u8>)>,
) -> Option<&'a str> {
match (tlv.class, tlv.tag) {
(TagClass::Universal, 2) => determine_integer_label(parent_ctx, child_index, last_child),
(TagClass::Universal, 3) => determine_bit_string_label(parent_ctx, child_index, last_child),
(TagClass::Universal, 4) => determine_octet_string_label(last_child),
_ => None,
}
}
fn determine_integer_label(
parent_ctx: ParentCtx,
child_index: usize,
last_child: &Option<(TagClass, bool, u64, Vec<u8>)>,
) -> Option<&'static str> {
match parent_ctx {
ParentCtx::TbsCertVersion => Some("version"),
ParentCtx::TbsCertificate => {
// serialNumber is the first INTEGER, or the second if [0] EXPLICIT version present
if child_index == 0 {
Some("serialNumber")
} else if child_index == 1 {
if let Some((prev_class, prev_constructed, prev_tag, _)) = last_child {
if *prev_class == TagClass::ContextSpecific && *prev_constructed && *prev_tag == 0 {
return Some("serialNumber");
}
}
None
} else {
None
}
}
_ => None,
}
}
fn determine_bit_string_label(
parent_ctx: ParentCtx,
child_index: usize,
last_child: &Option<(TagClass, bool, u64, Vec<u8>)>,
) -> Option<&'static str> {
match parent_ctx {
ParentCtx::Root => {
if child_index == 2 {
Some("signatureValue")
} else {
None
}
ParentCtx::Root | ParentCtx::Certificate => {
if child_index == 2 { Some("signatureValue") } else { None }
}
ParentCtx::Sequence => {
if child_index == 1 {

View File

@ -75,3 +75,4 @@ pub fn human_generalizedtime(s: &str) -> Option<String> {
dt.second()
))
}

View File

@ -1,9 +1,9 @@
use crate::asn1_types::{as_i128, decode_oid, ip_to_string, TagClass, TlvInfo};
use crate::certificate_utils::decode_keyusage_from_octet;
use super::asn1_types::{as_i128, decode_oid, ip_to_string, TagClass, TlvInfo};
use super::certificate_utils::decode_keyusage_from_octet;
use crate::color::{paint, CYAN, MAGENTA, YELLOW};
use crate::encoding_utils::{bits_to_hex_truncated, bits_to_string_truncated, to_hex_upper, BitsDisplay, BitsFormat};
use crate::oid_registry::oid_friendly;
use crate::time_utils::{human_generalizedtime, human_utctime};
use super::encoding_utils::{bits_to_hex_truncated, bits_to_string_truncated, to_hex_upper, BitsDisplay, BitsFormat};
use super::oid_registry::oid_friendly;
use super::time_utils::{human_generalizedtime, human_utctime};
#[derive(Copy, Clone)]
pub struct PrintCtx {
@ -29,7 +29,7 @@ pub fn print_primitive_value_with_label(
match (tlv.class, tlv.tag) {
(TagClass::Universal, 1) => print_boolean(content, ctx.color),
(TagClass::Universal, 2) => print_integer(content, ctx.color),
(TagClass::Universal, 2) => print_integer(content, label, ctx.color),
(TagClass::Universal, 3) => print_bit_string(content, label, ctx),
(TagClass::Universal, 4) => print_octet_string(content, label, ctx),
(TagClass::Universal, 5) => print_null(ctx.color),
@ -57,13 +57,28 @@ fn print_boolean(content: &[u8], color: bool) {
);
}
fn print_integer(content: &[u8], color: bool) {
fn print_integer(content: &[u8], label: Option<&str>, color: bool) {
if let Some(v) = as_i128(content) {
if matches!(label, Some("version")) {
let ver_name = match v {
0 => "v1",
1 => "v2",
2 => "v3",
_ => "unknown",
};
println!(
"{} {} ({})",
paint("INTEGER", CYAN, color),
paint(&v.to_string(), YELLOW, color),
paint(ver_name, YELLOW, color)
);
} else {
println!(
"{} {}",
paint("INTEGER", CYAN, color),
paint(&v.to_string(), YELLOW, color)
);
}
} else {
println!(
"{} 0x{}",
@ -74,8 +89,8 @@ fn print_integer(content: &[u8], color: bool) {
}
fn print_bit_string(content: &[u8], label: Option<&str>, ctx: PrintCtx) {
use crate::asn1_types::{looks_like_single_der, ParentCtx};
use crate::pretty_printer::pretty_print_der;
use super::asn1_types::{looks_like_single_der, ParentCtx};
use super::pretty_printer::pretty_print_der;
let unused = content.first().copied().unwrap_or(0) as usize;
let bits = content.get(1..).unwrap_or(&[]);
@ -123,8 +138,8 @@ fn print_bit_string(content: &[u8], label: Option<&str>, ctx: PrintCtx) {
}
fn print_octet_string(content: &[u8], label: Option<&str>, ctx: PrintCtx) {
use crate::asn1_types::{looks_like_single_der, ParentCtx};
use crate::pretty_printer::pretty_print_der;
use super::asn1_types::{looks_like_single_der, ParentCtx};
use super::pretty_printer::pretty_print_der;
let title = if let Some(l) = label {
format!("{} {}", l, "OCTET STRING")