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:
parent
e18612fd4a
commit
3bf914ee15
15
src/main.rs
15
src/main.rs
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -206,3 +206,4 @@ mod tests {
|
|||
assert!(truncated);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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(())
|
||||
|
|
@ -64,3 +64,4 @@ pub fn oid_friendly(oid: &str) -> Option<&'static str> {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
@ -75,3 +75,4 @@ pub fn human_generalizedtime(s: &str) -> Option<String> {
|
|||
dt.second()
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -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")
|
||||
Loading…
Reference in New Issue