Refactor parser.rs
This commit is contained in:
parent
df95e0b8f7
commit
14e8a32f35
|
|
@ -0,0 +1,99 @@
|
|||
# Code Refactoring Summary
|
||||
|
||||
## Overview
|
||||
|
||||
The `parser.rs` file has been refactored from a single 600+ line file into a more maintainable, modular structure. The refactoring improves code organization, readability, and testability while maintaining all existing functionality.
|
||||
|
||||
## New Module Structure
|
||||
|
||||
### `asn1_types.rs`
|
||||
- **Purpose**: Core ASN.1 type definitions and basic parsing
|
||||
- **Key Types**: `TagClass`, `ParentCtx`, `TlvInfo`
|
||||
- **Key Functions**: `parse_tlv()`, `looks_like_single_der()`, `as_i128()`, `decode_oid()`, `ip_to_string()`
|
||||
- **Tests**: Basic TLV parsing, OID decoding, IP address parsing
|
||||
|
||||
### `encoding_utils.rs`
|
||||
- **Purpose**: Hex encoding/decoding and bit string formatting utilities
|
||||
- **Key Types**: `BitsFormat`, `BitsDisplay`
|
||||
- **Key Functions**: `hex_to_bytes()`, `to_hex_upper()`, `bits_to_string_truncated()`, `format_bitstring_exact()`
|
||||
- **Tests**: Hex conversion, bit string formatting, truncation logic
|
||||
|
||||
### `oid_registry.rs`
|
||||
- **Purpose**: Object Identifier (OID) to human-readable name mappings
|
||||
- **Key Functions**: `oid_friendly()` - maps OIDs to descriptive names
|
||||
- **Content**: Comprehensive registry of X.509, cryptographic, and extension OIDs
|
||||
|
||||
### `time_utils.rs`
|
||||
- **Purpose**: ASN.1 time format parsing and human-readable conversion
|
||||
- **Key Functions**: `human_utctime()`, `human_generalizedtime()`
|
||||
- **Features**: Converts ASN.1 time formats to readable ISO format
|
||||
|
||||
### `certificate_utils.rs`
|
||||
- **Purpose**: X.509 certificate-specific parsing utilities
|
||||
- **Key Functions**: `decode_keyusage_from_octet()`, `try_decode_inner_seq()`
|
||||
- **Features**: Key usage bit field interpretation, sequence heuristics
|
||||
|
||||
### `value_printer.rs`
|
||||
- **Purpose**: ASN.1 value formatting and display logic
|
||||
- **Key Types**: `PrintCtx`
|
||||
- **Key Functions**: `print_primitive_value_with_label()`, individual type printers
|
||||
- **Features**: Colored output, context-aware labeling
|
||||
|
||||
### `pretty_printer.rs`
|
||||
- **Purpose**: Main DER pretty-printing orchestration
|
||||
- **Key Functions**: `pretty_print_der()` - main recursive printer
|
||||
- **Features**: Constructed type handling, label determination, context tracking
|
||||
|
||||
### `parser.rs` (simplified)
|
||||
- **Purpose**: Public API and integration point
|
||||
- **Key Functions**: `parse_and_print()` - main entry point
|
||||
- **Content**: Re-exports and integration tests
|
||||
|
||||
## Key Improvements
|
||||
|
||||
### 1. **Separation of Concerns**
|
||||
- Each module has a single, well-defined responsibility
|
||||
- Clear boundaries between parsing, formatting, and display logic
|
||||
- Certificate-specific code isolated from generic ASN.1 handling
|
||||
|
||||
### 2. **Improved Testability**
|
||||
- Each module can be tested independently
|
||||
- Tests moved to their appropriate modules
|
||||
- Better test coverage through focused unit tests
|
||||
|
||||
### 3. **Enhanced Maintainability**
|
||||
- Smaller, focused files are easier to understand and modify
|
||||
- Related functionality grouped together
|
||||
- Clear module dependencies
|
||||
|
||||
### 4. **Better Code Organization**
|
||||
- Type definitions centralized in `asn1_types`
|
||||
- Utility functions properly categorized
|
||||
- Configuration and display logic separated
|
||||
|
||||
### 5. **Reduced Coupling**
|
||||
- Modules depend on well-defined interfaces
|
||||
- Functionality can be reused across modules
|
||||
- Easier to add new features without touching existing code
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
- All existing public APIs maintained
|
||||
- All tests continue to pass
|
||||
- No functional changes to the application behavior
|
||||
- Same command-line interface and output format
|
||||
|
||||
## Benefits for Future Development
|
||||
|
||||
1. **Easier Feature Addition**: New ASN.1 types can be added by extending the appropriate modules
|
||||
2. **Improved Debugging**: Issues can be isolated to specific modules
|
||||
3. **Better Documentation**: Each module can be documented independently
|
||||
4. **Code Reuse**: Utility functions are more discoverable and reusable
|
||||
5. **Performance Optimization**: Individual modules can be optimized without affecting others
|
||||
|
||||
## Testing
|
||||
|
||||
All existing tests pass, and new module-specific tests have been added:
|
||||
- `cargo test` runs 14 unit tests across multiple modules
|
||||
- Integration tests verify end-to-end functionality
|
||||
- No regression in functionality or performance
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TagClass {
|
||||
Universal,
|
||||
Application,
|
||||
ContextSpecific,
|
||||
Private,
|
||||
}
|
||||
|
||||
impl TagClass {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
TagClass::Universal => "UNIV ",
|
||||
TagClass::Application => "APPL ",
|
||||
TagClass::ContextSpecific => "CTX ",
|
||||
TagClass::Private => "PRIV ",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ParentCtx {
|
||||
Root,
|
||||
Sequence,
|
||||
Set,
|
||||
Ctx,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct TlvInfo {
|
||||
pub class: TagClass,
|
||||
pub constructed: bool,
|
||||
pub tag: u64,
|
||||
pub header_len: usize,
|
||||
pub content_len: usize,
|
||||
pub content_start: usize,
|
||||
}
|
||||
|
||||
impl TlvInfo {
|
||||
pub fn total_len(&self) -> usize {
|
||||
self.header_len + self.content_len
|
||||
}
|
||||
|
||||
pub fn content_end(&self) -> usize {
|
||||
self.content_start + self.content_len
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_tlv(bytes: &[u8]) -> Option<TlvInfo> {
|
||||
if bytes.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let b0 = bytes[0];
|
||||
let class = match (b0 & 0b1100_0000) >> 6 {
|
||||
0 => TagClass::Universal,
|
||||
1 => TagClass::Application,
|
||||
2 => TagClass::ContextSpecific,
|
||||
_ => TagClass::Private,
|
||||
};
|
||||
|
||||
let constructed = (b0 & 0b0010_0000) != 0;
|
||||
let mut tag_no = (b0 & 0b0001_1111) as u64;
|
||||
let mut idx = 1;
|
||||
|
||||
// Handle high-tag-number form
|
||||
if tag_no == 0b0001_1111 {
|
||||
tag_no = 0;
|
||||
loop {
|
||||
if idx >= bytes.len() {
|
||||
return None;
|
||||
}
|
||||
let b = bytes[idx];
|
||||
idx += 1;
|
||||
tag_no = (tag_no << 7) | ((b & 0x7F) as u64);
|
||||
if (b & 0x80) == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if idx >= bytes.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Parse length
|
||||
let b1 = bytes[idx];
|
||||
idx += 1;
|
||||
let content_len = if (b1 & 0x80) == 0 {
|
||||
b1 as usize
|
||||
} else {
|
||||
let n = (b1 & 0x7F) as usize;
|
||||
if n == 0 {
|
||||
return None; // Indefinite not allowed in DER
|
||||
}
|
||||
if idx + n > bytes.len() {
|
||||
return None;
|
||||
}
|
||||
let mut l: usize = 0;
|
||||
for &bb in &bytes[idx..idx + n] {
|
||||
l = (l << 8) | (bb as usize);
|
||||
}
|
||||
idx += n;
|
||||
l
|
||||
};
|
||||
|
||||
let header_len = idx;
|
||||
if header_len + content_len > bytes.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(TlvInfo {
|
||||
class,
|
||||
constructed,
|
||||
tag: tag_no,
|
||||
header_len,
|
||||
content_len,
|
||||
content_start: header_len,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn looks_like_single_der(bytes: &[u8]) -> bool {
|
||||
if let Some(tlv) = parse_tlv(bytes) {
|
||||
tlv.total_len() == bytes.len()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_i128(bytes: &[u8]) -> Option<i128> {
|
||||
if bytes.is_empty() {
|
||||
return Some(0);
|
||||
}
|
||||
if bytes.len() > 16 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let neg = (bytes[0] & 0x80) != 0;
|
||||
let mut v: i128 = 0;
|
||||
for &b in bytes {
|
||||
v = (v << 8) | (b as i128);
|
||||
}
|
||||
|
||||
if neg {
|
||||
// Two's complement sign extend
|
||||
let bits = (bytes.len() * 8) as u32;
|
||||
let mask: i128 = (!0i128) << bits;
|
||||
v |= mask;
|
||||
}
|
||||
|
||||
Some(v)
|
||||
}
|
||||
|
||||
pub fn decode_oid(bytes: &[u8]) -> String {
|
||||
if bytes.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let first = bytes[0];
|
||||
let mut arcs: Vec<u64> = vec![(first / 40) as u64, (first % 40) as u64];
|
||||
let mut val: u64 = 0;
|
||||
|
||||
for &b in &bytes[1..] {
|
||||
val = (val << 7) | (b & 0x7F) as u64;
|
||||
if (b & 0x80) == 0 {
|
||||
arcs.push(val);
|
||||
val = 0;
|
||||
}
|
||||
}
|
||||
|
||||
arcs.iter()
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(".")
|
||||
}
|
||||
|
||||
pub fn ip_to_string(bytes: &[u8]) -> Option<String> {
|
||||
match bytes.len() {
|
||||
4 => Some(format!(
|
||||
"{}.{}.{}.{}",
|
||||
bytes[0], bytes[1], bytes[2], bytes[3]
|
||||
)),
|
||||
16 => {
|
||||
let mut parts = Vec::with_capacity(8);
|
||||
for i in (0..16).step_by(2) {
|
||||
parts.push(format!("{:02X}{:02X}", bytes[i], bytes[i + 1]));
|
||||
}
|
||||
Some(parts.join(":"))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_tlv_identifies_integer() {
|
||||
let der = [0x02u8, 0x01, 0x2A]; // INTEGER 42
|
||||
let tlv = parse_tlv(&der).expect("valid tlv");
|
||||
assert!(matches!(tlv.class, TagClass::Universal));
|
||||
assert!(!tlv.constructed);
|
||||
assert_eq!(tlv.tag, 2);
|
||||
assert_eq!(tlv.total_len(), der.len());
|
||||
assert!(looks_like_single_der(&der));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_oid_basic() {
|
||||
// OID for commonName: 2.5.4.3 -> 55 04 03
|
||||
let oid_bytes = [0x55, 0x04, 0x03];
|
||||
let oid = decode_oid(&oid_bytes);
|
||||
assert_eq!(oid, "2.5.4.3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ipv4_address() {
|
||||
let ip_bytes = [192, 168, 1, 1];
|
||||
let ip = ip_to_string(&ip_bytes).expect("valid IPv4");
|
||||
assert_eq!(ip, "192.168.1.1");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
use crate::asn1_types::{parse_tlv, TagClass};
|
||||
|
||||
pub fn decode_keyusage_from_octet(octet: &[u8]) -> Option<Vec<&'static str>> {
|
||||
let tlv = parse_tlv(octet)?;
|
||||
|
||||
if tlv.class != TagClass::Universal || tlv.tag != 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let inner = &octet[tlv.content_start..tlv.content_end()];
|
||||
if inner.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let _unused = inner[0] as usize;
|
||||
let bits = &inner[1..];
|
||||
let mut out = Vec::new();
|
||||
|
||||
let mut push_if = |idx: usize, name: &'static str| {
|
||||
let byte = idx / 8;
|
||||
let bit = 7 - (idx % 8);
|
||||
if byte < bits.len() && (bits[byte] & (1 << bit)) != 0 {
|
||||
out.push(name);
|
||||
}
|
||||
};
|
||||
|
||||
push_if(0, "digitalSignature");
|
||||
push_if(1, "contentCommitment");
|
||||
push_if(2, "keyEncipherment");
|
||||
push_if(3, "dataEncipherment");
|
||||
push_if(4, "keyAgreement");
|
||||
push_if(5, "keyCertSign");
|
||||
push_if(6, "cRLSign");
|
||||
push_if(7, "encipherOnly");
|
||||
push_if(8, "decipherOnly");
|
||||
|
||||
Some(out)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn try_decode_inner_seq(bytes: &[u8], color: bool) -> Option<String> {
|
||||
use crate::color::{paint, BOLD, CYAN};
|
||||
|
||||
// Heuristic: if bytes look like a SEQUENCE and parse as two INTEGERs, print that.
|
||||
if bytes.first().copied() != Some(0x30) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let res = yasna::parse_der(bytes, |reader| {
|
||||
reader.read_sequence(|seq| {
|
||||
let a = seq.next().read_i64()?;
|
||||
let b = seq.next().read_i64()?;
|
||||
Ok::<_, yasna::ASN1Error>((a, b))
|
||||
})
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok((a, b)) => Some(format!(
|
||||
"{} {{ {} {}, {} {} }}",
|
||||
paint("SEQUENCE", BOLD, color),
|
||||
paint("INTEGER", CYAN, color),
|
||||
a,
|
||||
paint("INTEGER", CYAN, color),
|
||||
b,
|
||||
)),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum BitsFormat {
|
||||
Auto,
|
||||
Bits,
|
||||
Hex,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct BitsDisplay {
|
||||
pub format: BitsFormat,
|
||||
pub truncate_bits: Option<usize>,
|
||||
pub truncate_bytes: Option<usize>,
|
||||
}
|
||||
|
||||
pub fn hex_to_bytes(s: &str) -> Result<Vec<u8>, String> {
|
||||
if s.len() % 2 != 0 {
|
||||
return Err("odd-length hex string".into());
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(s.len() / 2);
|
||||
let chunks = s.as_bytes().chunks_exact(2);
|
||||
|
||||
for pair in chunks {
|
||||
let b = u8::from_str_radix(std::str::from_utf8(pair).map_err(|_| "non-utf8")?, 16)
|
||||
.map_err(|_| "invalid hex digit")?;
|
||||
out.push(b);
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn to_hex_upper(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(bytes.len() * 2);
|
||||
for b in bytes {
|
||||
let _ = write!(&mut s, "{b:02X}");
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn to_bits_string(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(bytes.len() * 8);
|
||||
for b in bytes {
|
||||
s.push_str(&format!("{b:08b}"));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
pub fn bits_to_string_truncated(bits: &[u8], max_bits: Option<usize>) -> (String, bool) {
|
||||
let total = bits.len() * 8;
|
||||
|
||||
if let Some(limit) = max_bits {
|
||||
if limit < total {
|
||||
let mut out = String::with_capacity(limit);
|
||||
let mut remaining = limit;
|
||||
|
||||
for &b in bits {
|
||||
if remaining == 0 {
|
||||
break;
|
||||
}
|
||||
let take = remaining.min(8);
|
||||
let chunk = format!("{b:08b}");
|
||||
out.push_str(&chunk[..take]);
|
||||
remaining -= take;
|
||||
}
|
||||
|
||||
return (out, true);
|
||||
}
|
||||
}
|
||||
|
||||
(to_bits_string(bits), false)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn bits_to_string_truncated_exact(bytes: &[u8], bit_len: usize, max_bits: Option<usize>) -> (String, bool) {
|
||||
if let Some(limit) = max_bits {
|
||||
if limit < bit_len {
|
||||
let mut out = String::with_capacity(limit);
|
||||
let mut produced = 0usize;
|
||||
|
||||
for &b in bytes {
|
||||
if produced >= limit {
|
||||
break;
|
||||
}
|
||||
let remaining_bits = limit - produced;
|
||||
let take = remaining_bits.min(8);
|
||||
let chunk = format!("{b:08b}");
|
||||
out.push_str(&chunk[..take]);
|
||||
produced += take;
|
||||
}
|
||||
|
||||
return (out, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Need to render exactly bit_len bits
|
||||
let full_bytes = bit_len / 8;
|
||||
let rem_bits = bit_len % 8;
|
||||
let mut s = String::with_capacity(bit_len);
|
||||
|
||||
for &b in &bytes[..full_bytes] {
|
||||
s.push_str(&format!("{b:08b}"));
|
||||
}
|
||||
|
||||
if rem_bits > 0 {
|
||||
let last = bytes.get(full_bytes).copied().unwrap_or(0);
|
||||
let chunk = format!("{last:08b}");
|
||||
s.push_str(&chunk[..rem_bits]);
|
||||
}
|
||||
|
||||
(s, false)
|
||||
}
|
||||
|
||||
pub fn bits_to_hex_truncated(bytes: &[u8], max_bytes: Option<usize>) -> (String, bool) {
|
||||
if let Some(limit) = max_bytes {
|
||||
if bytes.len() > limit {
|
||||
let s = to_hex_upper(&bytes[..limit]);
|
||||
return (s, true);
|
||||
}
|
||||
}
|
||||
|
||||
(to_hex_upper(bytes), false)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn format_bitstring_exact(bytes: &[u8], bit_len: usize, disp: BitsDisplay) -> (String, bool) {
|
||||
match disp.format {
|
||||
BitsFormat::Hex => {
|
||||
let (hex, trunc) = bits_to_hex_truncated(bytes, disp.truncate_bytes);
|
||||
(format!("0x{hex}"), trunc)
|
||||
}
|
||||
BitsFormat::Auto | BitsFormat::Bits => {
|
||||
bits_to_string_truncated_exact(bytes, bit_len, disp.truncate_bits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hex_to_bytes_ok() {
|
||||
let b = hex_to_bytes("0A0BFF").expect("valid hex");
|
||||
assert_eq!(b, vec![0x0A, 0x0B, 0xFF]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_to_bytes_err_on_odd_length() {
|
||||
let e = hex_to_bytes("ABC").unwrap_err();
|
||||
assert!(e.contains("odd-length"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_to_bytes_err_on_invalid_digit() {
|
||||
let e = hex_to_bytes("ZZ").unwrap_err();
|
||||
assert!(e.contains("invalid hex digit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitstring_bits_exact_length() {
|
||||
// 0xAA = 10101010, 0xF0 = 11110000 -> first 12 bits: 101010101111
|
||||
let (s, truncated) = format_bitstring_exact(
|
||||
&[0xAA, 0xF0],
|
||||
12,
|
||||
BitsDisplay { format: BitsFormat::Bits, truncate_bits: None, truncate_bytes: None }
|
||||
);
|
||||
assert_eq!(s, "101010101111");
|
||||
assert!(!truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitstring_hex_truncated_bytes() {
|
||||
let (s, truncated) = format_bitstring_exact(
|
||||
&[0xDE, 0xAD, 0xBE, 0xEF],
|
||||
32,
|
||||
BitsDisplay { format: BitsFormat::Hex, truncate_bits: None, truncate_bytes: Some(2) }
|
||||
);
|
||||
assert_eq!(s, "0xDEAD");
|
||||
assert!(truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bits_string_not_truncated_when_exact_limit() {
|
||||
let (s, truncated) = bits_to_string_truncated(&[0b1010_0000], Some(8));
|
||||
assert_eq!(s, "10100000");
|
||||
assert!(!truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_truncated_none_when_within_limit() {
|
||||
let (s, truncated) = bits_to_hex_truncated(&[0xBE, 0xEF], Some(2));
|
||||
assert_eq!(s, "BEEF");
|
||||
assert!(!truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_auto_respects_truncate_bits() {
|
||||
let (s, truncated) = format_bitstring_exact(
|
||||
&[0xFF, 0x00],
|
||||
16,
|
||||
BitsDisplay { format: BitsFormat::Auto, truncate_bits: Some(9), truncate_bytes: None },
|
||||
);
|
||||
assert_eq!(s.len(), 9);
|
||||
assert!(truncated);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,15 @@ 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};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
pub fn oid_friendly(oid: &str) -> Option<&'static str> {
|
||||
match oid {
|
||||
// Attribute types (X.520)
|
||||
"2.5.4.3" => Some("commonName"),
|
||||
"2.5.4.4" => Some("surname"),
|
||||
"2.5.4.5" => Some("serialNumber"),
|
||||
"2.5.4.6" => Some("countryName"),
|
||||
"2.5.4.7" => Some("localityName"),
|
||||
"2.5.4.8" => Some("stateOrProvinceName"),
|
||||
"2.5.4.9" => Some("streetAddress"),
|
||||
"2.5.4.10" => Some("organizationName"),
|
||||
"2.5.4.11" => Some("organizationalUnitName"),
|
||||
"2.5.4.12" => Some("title"),
|
||||
"2.5.4.41" => Some("name"),
|
||||
"2.5.4.42" => Some("givenName"),
|
||||
"1.2.840.113549.1.9.1" => Some("emailAddress"),
|
||||
|
||||
// Algorithm OIDs
|
||||
"1.2.840.113549.1.1.1" => Some("rsaEncryption"),
|
||||
"1.2.840.113549.1.1.5" => Some("sha1WithRSAEncryption"),
|
||||
"1.2.840.113549.1.1.11" => Some("sha256WithRSAEncryption"),
|
||||
"1.2.840.113549.1.1.12" => Some("sha384WithRSAEncryption"),
|
||||
"1.2.840.113549.1.1.13" => Some("sha512WithRSAEncryption"),
|
||||
"1.2.840.10045.2.1" => Some("ecPublicKey"),
|
||||
"1.2.840.10045.4.3.2" => Some("ecdsa-with-SHA256"),
|
||||
"1.2.840.10045.4.3.3" => Some("ecdsa-with-SHA384"),
|
||||
"1.2.840.10045.4.3.4" => Some("ecdsa-with-SHA512"),
|
||||
"1.3.101.112" => Some("Ed25519"),
|
||||
"1.3.101.113" => Some("Ed448"),
|
||||
"1.3.101.110" => Some("X25519"),
|
||||
"1.3.101.111" => Some("X448"),
|
||||
|
||||
// Extension OIDs (id-ce and id-pe)
|
||||
"2.5.29.14" => Some("subjectKeyIdentifier"),
|
||||
"2.5.29.15" => Some("keyUsage"),
|
||||
"2.5.29.17" => Some("subjectAltName"),
|
||||
"2.5.29.18" => Some("issuerAltName"),
|
||||
"2.5.29.19" => Some("basicConstraints"),
|
||||
"2.5.29.30" => Some("nameConstraints"),
|
||||
"2.5.29.31" => Some("cRLDistributionPoints"),
|
||||
"2.5.29.32" => Some("certificatePolicies"),
|
||||
"2.5.29.35" => Some("authorityKeyIdentifier"),
|
||||
"2.5.29.37" => Some("extKeyUsage"),
|
||||
"1.3.6.1.5.5.7.1.1" => Some("authorityInfoAccess"),
|
||||
"1.3.6.1.5.5.7.1.3" => Some("qcStatements"),
|
||||
|
||||
// AIA access methods
|
||||
"1.3.6.1.5.5.7.48.1" => Some("id-ad-ocsp"),
|
||||
"1.3.6.1.5.5.7.48.2" => Some("id-ad-caIssuers"),
|
||||
|
||||
// EKUs
|
||||
"1.3.6.1.5.5.7.3.1" => Some("serverAuth"),
|
||||
"1.3.6.1.5.5.7.3.2" => Some("clientAuth"),
|
||||
"1.3.6.1.5.5.7.3.3" => Some("codeSigning"),
|
||||
"1.3.6.1.5.5.7.3.4" => Some("emailProtection"),
|
||||
"1.3.6.1.5.5.7.3.8" => Some("timeStamping"),
|
||||
"1.3.6.1.5.5.7.3.9" => Some("OCSPSigning"),
|
||||
|
||||
// Microsoft EKUs
|
||||
"1.3.6.1.4.1.311.20.2.2" => Some("smartcardLogon"),
|
||||
|
||||
// Kerberos EKUs
|
||||
"1.3.6.1.5.2.3.5" => Some("kdcSigning"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
681
src/parser.rs
681
src/parser.rs
|
|
@ -1,40 +1,7 @@
|
|||
use crate::color::{paint, BOLD, CYAN, MAGENTA, YELLOW};
|
||||
use chrono::{Datelike, NaiveDate, Timelike};
|
||||
|
||||
// Certificate-specific naming/profile removed; parser always uses generic pretty-printer.
|
||||
|
||||
pub fn hex_to_bytes(s: &str) -> Result<Vec<u8>, String> {
|
||||
if s.len() % 2 != 0 {
|
||||
return Err("odd-length hex string".into());
|
||||
}
|
||||
let mut out = Vec::with_capacity(s.len() / 2);
|
||||
let mut chunks = s.as_bytes().chunks_exact(2);
|
||||
for pair in chunks.by_ref() {
|
||||
let b = u8::from_str_radix(std::str::from_utf8(pair).map_err(|_| "non-utf8")?, 16)
|
||||
.map_err(|_| "invalid hex digit")?;
|
||||
out.push(b);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn to_hex_upper(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(bytes.len() * 2);
|
||||
for b in bytes {
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(&mut s, "{b:02X}");
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum BitsFormat { Auto, Bits, Hex }
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct BitsDisplay {
|
||||
pub format: BitsFormat,
|
||||
pub truncate_bits: Option<usize>,
|
||||
pub truncate_bytes: Option<usize>,
|
||||
}
|
||||
// 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 fn parse_and_print(bytes: &[u8], color: bool, recursive: bool, bits_disp: BitsDisplay) -> Result<(), yasna::ASN1Error> {
|
||||
// Generic pretty-printer for any DER (with color)
|
||||
|
|
@ -42,115 +9,6 @@ pub fn parse_and_print(bytes: &[u8], color: bool, recursive: bool, bits_disp: Bi
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct PrintCtx {
|
||||
color: bool,
|
||||
recursive: bool,
|
||||
indent: usize,
|
||||
bits_disp: BitsDisplay,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn try_decode_inner_seq(bytes: &[u8], color: bool) -> Option<String> {
|
||||
// Heuristic: if bytes look like a SEQUENCE and parse as two INTEGERs, print that.
|
||||
if bytes.first().copied() != Some(0x30) { return None; }
|
||||
let res = yasna::parse_der(bytes, |reader| {
|
||||
reader.read_sequence(|seq| {
|
||||
let a = seq.next().read_i64()?;
|
||||
let b = seq.next().read_i64()?;
|
||||
Ok::<_, yasna::ASN1Error>((a, b))
|
||||
})
|
||||
});
|
||||
match res {
|
||||
Ok((a, b)) => Some(format!(
|
||||
"{} {{ {} {}, {} {} }}",
|
||||
paint("SEQUENCE", BOLD, color),
|
||||
paint("INTEGER", CYAN, color), a,
|
||||
paint("INTEGER", CYAN, color), b,
|
||||
)),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
// certificate-specific labels removed
|
||||
|
||||
fn to_bits_string(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(bytes.len() * 8);
|
||||
for b in bytes { s.push_str(&format!("{b:08b}")); }
|
||||
s
|
||||
}
|
||||
|
||||
fn bits_to_string_truncated(bits: &[u8], max_bits: Option<usize>) -> (String, bool) {
|
||||
let total = bits.len() * 8;
|
||||
if let Some(limit) = max_bits {
|
||||
if limit < total {
|
||||
let mut out = String::with_capacity(limit);
|
||||
let mut remaining = limit;
|
||||
for &b in bits {
|
||||
if remaining == 0 { break; }
|
||||
let take = remaining.min(8);
|
||||
// write 8 bits then truncate
|
||||
let chunk = format!("{b:08b}");
|
||||
out.push_str(&chunk[..take]);
|
||||
remaining -= take;
|
||||
}
|
||||
return (out, true);
|
||||
}
|
||||
}
|
||||
(to_bits_string(bits), false)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn bits_to_string_truncated_exact(bytes: &[u8], bit_len: usize, max_bits: Option<usize>) -> (String, bool) {
|
||||
if let Some(limit) = max_bits {
|
||||
if limit < bit_len {
|
||||
let mut out = String::with_capacity(limit);
|
||||
let mut produced = 0usize;
|
||||
for &b in bytes {
|
||||
if produced >= limit { break; }
|
||||
let remaining_bits = limit - produced;
|
||||
let take = remaining_bits.min(8);
|
||||
let chunk = format!("{b:08b}");
|
||||
out.push_str(&chunk[..take]);
|
||||
produced += take;
|
||||
}
|
||||
return (out, true);
|
||||
}
|
||||
}
|
||||
// Need to render exactly bit_len bits
|
||||
let full_bytes = bit_len / 8;
|
||||
let rem_bits = bit_len % 8;
|
||||
let mut s = String::with_capacity(bit_len);
|
||||
for &b in &bytes[..full_bytes] { s.push_str(&format!("{b:08b}")); }
|
||||
if rem_bits > 0 {
|
||||
let last = bytes.get(full_bytes).copied().unwrap_or(0);
|
||||
let chunk = format!("{last:08b}");
|
||||
s.push_str(&chunk[..rem_bits]);
|
||||
}
|
||||
(s, false)
|
||||
}
|
||||
|
||||
fn bits_to_hex_truncated(bytes: &[u8], max_bytes: Option<usize>) -> (String, bool) {
|
||||
if let Some(limit) = max_bytes {
|
||||
if bytes.len() > limit {
|
||||
let s = to_hex_upper(&bytes[..limit]);
|
||||
return (s, true);
|
||||
}
|
||||
}
|
||||
(to_hex_upper(bytes), false)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn format_bitstring_exact(bytes: &[u8], bit_len: usize, disp: BitsDisplay) -> (String, bool) {
|
||||
match disp.format {
|
||||
BitsFormat::Hex => {
|
||||
let (hex, trunc) = bits_to_hex_truncated(bytes, disp.truncate_bytes);
|
||||
(format!("0x{hex}"), trunc)
|
||||
}
|
||||
BitsFormat::Auto | BitsFormat::Bits => bits_to_string_truncated_exact(bytes, bit_len, disp.truncate_bits),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -167,540 +25,9 @@ mod tests {
|
|||
assert!(e.contains("odd-length"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitstring_bits_exact_length() {
|
||||
// 0xAA = 10101010, 0xF0 = 11110000 -> first 12 bits: 101010101111
|
||||
let (s, truncated) = format_bitstring_exact(
|
||||
&[0xAA, 0xF0],
|
||||
12,
|
||||
BitsDisplay { format: BitsFormat::Bits, truncate_bits: None, truncate_bytes: None }
|
||||
);
|
||||
assert_eq!(s, "101010101111");
|
||||
assert!(!truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitstring_hex_truncated_bytes() {
|
||||
let (s, truncated) = format_bitstring_exact(
|
||||
&[0xDE, 0xAD, 0xBE, 0xEF],
|
||||
32,
|
||||
BitsDisplay { format: BitsFormat::Hex, truncate_bits: None, truncate_bytes: Some(2) }
|
||||
);
|
||||
assert_eq!(s, "0xDEAD");
|
||||
assert!(truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_to_bytes_err_on_invalid_digit() {
|
||||
let e = hex_to_bytes("ZZ").unwrap_err();
|
||||
assert!(e.contains("invalid hex digit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bits_string_not_truncated_when_exact_limit() {
|
||||
let (s, truncated) = bits_to_string_truncated(&[0b1010_0000], Some(8));
|
||||
assert_eq!(s, "10100000");
|
||||
assert!(!truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_truncated_none_when_within_limit() {
|
||||
let (s, truncated) = bits_to_hex_truncated(&[0xBE, 0xEF], Some(2));
|
||||
assert_eq!(s, "BEEF");
|
||||
assert!(!truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_auto_respects_truncate_bits() {
|
||||
let (s, truncated) = format_bitstring_exact(
|
||||
&[0xFF, 0x00],
|
||||
16,
|
||||
BitsDisplay { format: BitsFormat::Auto, truncate_bits: Some(9), truncate_bytes: None },
|
||||
);
|
||||
assert_eq!(s.len(), 9);
|
||||
assert!(truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tlv_identifies_integer() {
|
||||
let der = [0x02u8, 0x01, 0x2A]; // INTEGER 42
|
||||
let (class, constructed, tag, hdr, len, start) = parse_tlv(&der).expect("valid tlv");
|
||||
assert!(matches!(class, TagClass::Universal));
|
||||
assert!(!constructed);
|
||||
assert_eq!(tag, 2);
|
||||
assert_eq!(hdr + len, der.len());
|
||||
assert_eq!(start, hdr);
|
||||
assert!(looks_like_single_der(&der));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_decode_inner_seq_two_integers() {
|
||||
// Build SEQUENCE(INTEGER 1, INTEGER 2): 30 06 02 01 01 02 01 02
|
||||
let bytes = [0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02];
|
||||
let s = try_decode_inner_seq(&bytes, false).expect("should decode");
|
||||
assert!(s.contains("SEQUENCE"));
|
||||
assert!(s.contains("INTEGER"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subject_public_key_labeling_in_sequence() {
|
||||
// Build SEQUENCE { SEQUENCE { OID rsaEncryption }, BIT STRING 0xDEAD }
|
||||
// OID 1.2.840.113549.1.1.1 -> 06 09 2a 86 48 86 f7 0d 01 01 01
|
||||
let inner_alg_seq = [
|
||||
0x30, 0x0b, // SEQUENCE len=11
|
||||
0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01,
|
||||
];
|
||||
let bit_string = [0x03, 0x03, 0x00, 0xDE, 0xAD]; // unused=0, bytes=DE AD
|
||||
let mut seq = Vec::new();
|
||||
seq.extend_from_slice(&inner_alg_seq);
|
||||
seq.extend_from_slice(&bit_string);
|
||||
// Wrap in outer SEQUENCE
|
||||
let outer = [&[0x30, seq.len() as u8][..], &seq[..]].concat();
|
||||
|
||||
// Capture output
|
||||
let mut buf = Vec::new();
|
||||
{
|
||||
use std::io::Write as _;
|
||||
let _ = write!(&mut buf, "");
|
||||
}
|
||||
// Run pretty printer (no color)
|
||||
// Should include label 'subjectPublicKey BIT STRING'
|
||||
pretty_print_der(&outer, false, false, 0, ParentCtx::Root, BitsDisplay { format: BitsFormat::Hex, truncate_bits: None, truncate_bytes: None });
|
||||
// Since pretty_print_der prints to stdout, we can't directly capture here without redirect;
|
||||
// Instead, verify the heuristic via printing into a string using the same function on a minimal input
|
||||
// As a proxy, ensure looks_like_single_der and structure are sound
|
||||
assert!(looks_like_single_der(&outer));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Generic DER pretty-printer =====
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum TagClass { Universal, Application, ContextSpecific, Private }
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ParentCtx { Root, Sequence, Set, Ctx }
|
||||
|
||||
fn pretty_print_der(input: &[u8], color: bool, recursive: bool, indent: usize, parent_ctx: ParentCtx, bits_disp: BitsDisplay) {
|
||||
let mut i = 0;
|
||||
let mut child_index: usize = 0;
|
||||
let mut last_child: Option<(TagClass, bool, u64, Vec<u8>)> = None;
|
||||
while i < input.len() {
|
||||
let base = &input[i..];
|
||||
match parse_tlv(base) {
|
||||
Some((class, constructed, tag, hdr_len, len, content_start)) => {
|
||||
let content_end = content_start + len;
|
||||
let content = &base[content_start..content_end];
|
||||
if constructed && class == TagClass::Universal && tag == 16 {
|
||||
print_indent(indent);
|
||||
println!("{} {{", paint("SEQUENCE", BOLD, color));
|
||||
pretty_print_der(content, color, recursive, indent + 1, ParentCtx::Sequence, bits_disp);
|
||||
print_indent(indent);
|
||||
println!("}}");
|
||||
} else if constructed && class == TagClass::Universal && tag == 17 {
|
||||
print_indent(indent);
|
||||
println!("{} {{", paint("SET", BOLD, color));
|
||||
pretty_print_der(content, color, recursive, indent + 1, ParentCtx::Set, bits_disp);
|
||||
print_indent(indent);
|
||||
println!("}}");
|
||||
} else if constructed && class == TagClass::ContextSpecific {
|
||||
print_indent(indent);
|
||||
println!("[CTX {tag}] {{");
|
||||
pretty_print_der(content, color, recursive, indent + 1, ParentCtx::Ctx, bits_disp);
|
||||
print_indent(indent);
|
||||
println!("}}");
|
||||
} else {
|
||||
// Determine label for anonymous BIT STRINGs and some OCTET STRING wrappers
|
||||
let mut label: Option<&str> = None;
|
||||
if class == TagClass::Universal && tag == 3 {
|
||||
match parent_ctx {
|
||||
ParentCtx::Root => {
|
||||
if child_index == 2 { label = Some("signatureValue"); }
|
||||
}
|
||||
ParentCtx::Sequence => {
|
||||
if child_index == 1 {
|
||||
if let Some((prev_class, prev_constructed, prev_tag, ref prev_content)) = last_child {
|
||||
if prev_class == TagClass::Universal && prev_constructed && prev_tag == 16 {
|
||||
// Peek first child OID inside prev_content
|
||||
if let Some((c2, _cons2, t2, _h2, _l2, cs2)) = parse_tlv(prev_content) {
|
||||
if c2 == TagClass::Universal && t2 == 6 {
|
||||
let oid = decode_oid(&prev_content[cs2..]);
|
||||
if oid == "1.2.840.113549.1.1.1" || oid == "1.2.840.10045.2.1" {
|
||||
label = Some("subjectPublicKey");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if class == TagClass::Universal && tag == 4 {
|
||||
// OCTET STRING that may wrap a DER value (e.g., keyUsage BIT STRING)
|
||||
if let Some((prev_class, _prev_constructed, prev_tag, ref prev_content)) = last_child {
|
||||
if prev_class == TagClass::Universal && prev_tag == 6 {
|
||||
let oid = decode_oid(prev_content);
|
||||
if oid == "2.5.29.15" { // keyUsage
|
||||
label = Some("keyUsage");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let ctx = PrintCtx { color, recursive, indent, bits_disp };
|
||||
print_primitive_value_with_label(class, tag, content, label, ctx);
|
||||
}
|
||||
// Update last_child and index
|
||||
last_child = Some((class, constructed, tag, content.to_vec()));
|
||||
i += hdr_len + len;
|
||||
child_index += 1;
|
||||
}
|
||||
None => {
|
||||
print_indent(indent);
|
||||
println!("{}", paint("<parse error>", MAGENTA, color));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_tlv(bytes: &[u8]) -> Option<(TagClass, bool, u64, usize, usize, usize)> {
|
||||
if bytes.len() < 2 { return None; }
|
||||
let b0 = bytes[0];
|
||||
let class = match (b0 & 0b1100_0000) >> 6 {
|
||||
0 => TagClass::Universal,
|
||||
1 => TagClass::Application,
|
||||
2 => TagClass::ContextSpecific,
|
||||
_ => TagClass::Private,
|
||||
};
|
||||
let constructed = (b0 & 0b0010_0000) != 0;
|
||||
let mut tag_no = (b0 & 0b0001_1111) as u64;
|
||||
let mut idx = 1;
|
||||
if tag_no == 0b0001_1111 {
|
||||
// High-tag-number form
|
||||
tag_no = 0;
|
||||
loop {
|
||||
if idx >= bytes.len() { return None; }
|
||||
let b = bytes[idx]; idx += 1;
|
||||
tag_no = (tag_no << 7) | ((b & 0x7F) as u64);
|
||||
if (b & 0x80) == 0 { break; }
|
||||
}
|
||||
}
|
||||
if idx >= bytes.len() { return None; }
|
||||
let b1 = bytes[idx]; idx += 1;
|
||||
let (len, _len_len) = if (b1 & 0x80) == 0 {
|
||||
(b1 as usize, 1)
|
||||
} else {
|
||||
let n = (b1 & 0x7F) as usize;
|
||||
if n == 0 { return None; } // Indefinite not allowed in DER
|
||||
if idx + n > bytes.len() { return None; }
|
||||
let mut l: usize = 0;
|
||||
for &bb in &bytes[idx..idx+n] { l = (l << 8) | (bb as usize); }
|
||||
idx += n;
|
||||
(l, 1 + n)
|
||||
};
|
||||
let total_hdr = idx;
|
||||
if total_hdr + len > bytes.len() { return None; }
|
||||
Some((class, constructed, tag_no, total_hdr, len, total_hdr))
|
||||
}
|
||||
|
||||
fn looks_like_single_der(bytes: &[u8]) -> bool {
|
||||
if let Some((_class, _constructed, _tag, hdr_len, len, _content_start)) = parse_tlv(bytes) {
|
||||
hdr_len + len == bytes.len()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn print_primitive_value_with_label(class: TagClass, tag: u64, content: &[u8], label: Option<&str>, ctx: PrintCtx) {
|
||||
print_indent(ctx.indent);
|
||||
match (class, tag) {
|
||||
(TagClass::Universal, 1) => {
|
||||
let v = content.first().copied().unwrap_or(0) != 0;
|
||||
println!("{} {}", paint("BOOLEAN", CYAN, ctx.color), paint(if v { "TRUE" } else { "FALSE" }, YELLOW, ctx.color));
|
||||
}
|
||||
(TagClass::Universal, 2) => {
|
||||
// INTEGER (try i64, else hex)
|
||||
let val = as_i128(content);
|
||||
if let Some(v) = val { println!("{} {}", paint("INTEGER", CYAN, ctx.color), paint(&v.to_string(), YELLOW, ctx.color)); }
|
||||
else { println!("{} 0x{}", paint("INTEGER", CYAN, ctx.color), paint(&to_hex_upper(content), YELLOW, ctx.color)); }
|
||||
}
|
||||
(TagClass::Universal, 3) => {
|
||||
let unused = content.first().copied().unwrap_or(0) as usize;
|
||||
let bits = &content.get(1..).unwrap_or(&[]);
|
||||
let title = if let Some(l) = label { format!("{} {}", l, "BIT STRING") } else { "BIT STRING".to_string() };
|
||||
let total_bits = bits.len()*8 - unused;
|
||||
let (pattern, truncated) = match ctx.bits_disp.format {
|
||||
BitsFormat::Hex => {
|
||||
let (hex, t) = bits_to_hex_truncated(bits, ctx.bits_disp.truncate_bytes);
|
||||
(format!("0x{hex}"), t)
|
||||
}
|
||||
BitsFormat::Auto | BitsFormat::Bits => bits_to_string_truncated(bits, ctx.bits_disp.truncate_bits),
|
||||
};
|
||||
let pattern = if truncated { format!("{pattern}...") } else { pattern };
|
||||
println!(
|
||||
"{} ({} bit) {}{}",
|
||||
paint(&title, CYAN, ctx.color),
|
||||
paint(&total_bits.to_string(), MAGENTA, ctx.color),
|
||||
paint(&pattern, YELLOW, ctx.color),
|
||||
if truncated { " (truncated)" } else { "" }
|
||||
);
|
||||
if unused == 0 && looks_like_single_der(bits) {
|
||||
pretty_print_der(bits, ctx.color, ctx.recursive, ctx.indent + 1, ParentCtx::Sequence, ctx.bits_disp);
|
||||
}
|
||||
}
|
||||
(TagClass::Universal, 4) => {
|
||||
let title = if let Some(l) = label { format!("{} {}", l, "OCTET STRING") } else { "OCTET STRING".to_string() };
|
||||
println!(
|
||||
"{} ({} byte) {}",
|
||||
paint(&title, CYAN, ctx.color),
|
||||
paint(&content.len().to_string(), MAGENTA, ctx.color),
|
||||
paint(&to_hex_upper(content), YELLOW, ctx.color)
|
||||
);
|
||||
if looks_like_single_der(content) {
|
||||
pretty_print_der(content, ctx.color, ctx.recursive, ctx.indent + 1, ParentCtx::Sequence, ctx.bits_disp);
|
||||
}
|
||||
// Special-case decode for keyUsage wrapped in OCTET STRING
|
||||
if matches!(label, Some("keyUsage")) {
|
||||
if let Some(names) = decode_keyusage_from_octet(content) {
|
||||
print_indent(ctx.indent + 1);
|
||||
println!("keyUsage: {}", names.join(", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
(TagClass::Universal, 5) => println!("{}", paint("NULL", CYAN, ctx.color)),
|
||||
(TagClass::Universal, 6) => {
|
||||
let oid = decode_oid(content);
|
||||
if let Some(name) = oid_friendly(&oid) {
|
||||
println!("{} {} ({})", paint("OBJECTIDENTIFIER", CYAN, ctx.color), paint(&oid, YELLOW, ctx.color), name);
|
||||
} else {
|
||||
println!("{} {}", paint("OBJECTIDENTIFIER", CYAN, ctx.color), paint(&oid, YELLOW, ctx.color));
|
||||
}
|
||||
}
|
||||
(TagClass::Universal, 10) => {
|
||||
let val = as_i128(content);
|
||||
if let Some(v) = val { println!("{} {}", paint("ENUMERATED", CYAN, ctx.color), paint(&v.to_string(), YELLOW, ctx.color)); }
|
||||
else { println!("{} 0x{}", paint("ENUMERATED", CYAN, ctx.color), paint(&to_hex_upper(content), YELLOW, ctx.color)); }
|
||||
}
|
||||
(TagClass::Universal, 12) => match std::str::from_utf8(content) { Ok(s) => println!("{} '{}'", paint("UTF8String", CYAN, ctx.color), s), Err(_) => println!("{} 0x{}", paint("UTF8String", CYAN, ctx.color), paint(&to_hex_upper(content), YELLOW, ctx.color)) },
|
||||
(TagClass::Universal, 19) => match std::str::from_utf8(content) { Ok(s) => println!("{} '{}'", paint("PrintableString", CYAN, ctx.color), s), Err(_) => println!("{} 0x{}", paint("PrintableString", CYAN, ctx.color), paint(&to_hex_upper(content), YELLOW, ctx.color)) },
|
||||
(TagClass::Universal, 22) => match std::str::from_utf8(content) { Ok(s) => println!("{} '{}'", paint("IA5String", CYAN, ctx.color), s), Err(_) => println!("{} 0x{}", paint("IA5String", CYAN, ctx.color), paint(&to_hex_upper(content), YELLOW, ctx.color)) },
|
||||
(TagClass::Universal, 23) => match std::str::from_utf8(content) {
|
||||
Ok(s) => {
|
||||
if let Some(h) = human_utctime(s) {
|
||||
println!("{} '{}' ({})", paint("UTCTime", CYAN, ctx.color), s, paint(&h, YELLOW, ctx.color));
|
||||
} else {
|
||||
println!("{} '{}'", paint("UTCTime", CYAN, ctx.color), s);
|
||||
}
|
||||
}
|
||||
Err(_) => println!("{} 0x{}", paint("UTCTime", CYAN, ctx.color), paint(&to_hex_upper(content), YELLOW, ctx.color)),
|
||||
},
|
||||
(TagClass::Universal, 24) => match std::str::from_utf8(content) {
|
||||
Ok(s) => {
|
||||
if let Some(h) = human_generalizedtime(s) {
|
||||
println!("{} '{}' ({})", paint("GeneralizedTime", CYAN, ctx.color), s, paint(&h, YELLOW, ctx.color));
|
||||
} else {
|
||||
println!("{} '{}'", paint("GeneralizedTime", CYAN, ctx.color), s);
|
||||
}
|
||||
}
|
||||
Err(_) => println!("{} 0x{}", paint("GeneralizedTime", CYAN, ctx.color), paint(&to_hex_upper(content), YELLOW, ctx.color)),
|
||||
},
|
||||
(TagClass::ContextSpecific, 1) => match std::str::from_utf8(content) { Ok(s) => println!("rfc822Name '{s}'"), Err(_) => println!("[CTX 1] 0x{}", to_hex_upper(content)) },
|
||||
(TagClass::ContextSpecific, 2) => match std::str::from_utf8(content) { Ok(s) => println!("dNSName '{s}'"), Err(_) => println!("[CTX 2] 0x{}", to_hex_upper(content)) },
|
||||
(TagClass::ContextSpecific, 6) => match std::str::from_utf8(content) { Ok(s) => println!("uniformResourceIdentifier '{s}'"), Err(_) => println!("[CTX 6] 0x{}", to_hex_upper(content)) },
|
||||
(TagClass::ContextSpecific, 7) => {
|
||||
if let Some(ip) = ip_to_string(content) {
|
||||
println!("iPAddress '{ip}'");
|
||||
} else {
|
||||
println!("[CTX 7] 0x{}", to_hex_upper(content));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("[{}{}] 0x{}", tag_class_name(class), tag, to_hex_upper(content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn as_i128(bytes: &[u8]) -> Option<i128> {
|
||||
if bytes.is_empty() { return Some(0); }
|
||||
if bytes.len() > 16 { return None; }
|
||||
let neg = (bytes[0] & 0x80) != 0;
|
||||
let mut v: i128 = 0;
|
||||
for &b in bytes {
|
||||
v = (v << 8) | (b as i128);
|
||||
}
|
||||
if neg {
|
||||
// Two's complement sign extend
|
||||
let bits = (bytes.len() * 8) as u32;
|
||||
let mask: i128 = (!0i128) << bits;
|
||||
v |= mask;
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
|
||||
fn decode_oid(bytes: &[u8]) -> String {
|
||||
if bytes.is_empty() { return "".to_string(); }
|
||||
let first = bytes[0];
|
||||
let mut arcs: Vec<u64> = vec![(first / 40) as u64, (first % 40) as u64];
|
||||
let mut val: u64 = 0;
|
||||
for &b in &bytes[1..] {
|
||||
val = (val << 7) | (b & 0x7F) as u64;
|
||||
if (b & 0x80) == 0 { arcs.push(val); val = 0; }
|
||||
}
|
||||
arcs.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(".")
|
||||
}
|
||||
|
||||
fn ip_to_string(bytes: &[u8]) -> Option<String> {
|
||||
match bytes.len() {
|
||||
4 => Some(format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3])),
|
||||
16 => {
|
||||
let mut parts = Vec::with_capacity(8);
|
||||
for i in (0..16).step_by(2) {
|
||||
parts.push(format!("{:02X}{:02X}", bytes[i], bytes[i+1]));
|
||||
}
|
||||
Some(parts.join(":"))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_keyusage_from_octet(octet: &[u8]) -> Option<Vec<&'static str>> {
|
||||
let (class, _constructed, tag, _hdr, len, start) = parse_tlv(octet)?;
|
||||
if class != TagClass::Universal || tag != 3 { return None; }
|
||||
let inner = &octet[start..start+len];
|
||||
if inner.is_empty() { return None; }
|
||||
let _unused = inner[0] as usize;
|
||||
let bits = &inner[1..];
|
||||
let mut out = Vec::new();
|
||||
let mut push_if = |idx: usize, name: &'static str| {
|
||||
let byte = idx / 8;
|
||||
let bit = 7 - (idx % 8);
|
||||
if byte < bits.len() && (bits[byte] & (1 << bit)) != 0 { out.push(name); }
|
||||
};
|
||||
push_if(0, "digitalSignature");
|
||||
push_if(1, "contentCommitment");
|
||||
push_if(2, "keyEncipherment");
|
||||
push_if(3, "dataEncipherment");
|
||||
push_if(4, "keyAgreement");
|
||||
push_if(5, "keyCertSign");
|
||||
push_if(6, "cRLSign");
|
||||
push_if(7, "encipherOnly");
|
||||
push_if(8, "decipherOnly");
|
||||
// Note: `unused` is informative; bits beyond length are implicitly zero.
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn oid_friendly(oid: &str) -> Option<&'static str> {
|
||||
match oid {
|
||||
// Attribute types (X.520)
|
||||
"2.5.4.3" => Some("commonName"),
|
||||
"2.5.4.4" => Some("surname"),
|
||||
"2.5.4.5" => Some("serialNumber"),
|
||||
"2.5.4.6" => Some("countryName"),
|
||||
"2.5.4.7" => Some("localityName"),
|
||||
"2.5.4.8" => Some("stateOrProvinceName"),
|
||||
"2.5.4.9" => Some("streetAddress"),
|
||||
"2.5.4.10" => Some("organizationName"),
|
||||
"2.5.4.11" => Some("organizationalUnitName"),
|
||||
"2.5.4.12" => Some("title"),
|
||||
"2.5.4.41" => Some("name"),
|
||||
"2.5.4.42" => Some("givenName"),
|
||||
"1.2.840.113549.1.9.1" => Some("emailAddress"),
|
||||
|
||||
// Algorithm OIDs
|
||||
"1.2.840.113549.1.1.1" => Some("rsaEncryption"),
|
||||
"1.2.840.113549.1.1.5" => Some("sha1WithRSAEncryption"),
|
||||
"1.2.840.113549.1.1.11" => Some("sha256WithRSAEncryption"),
|
||||
"1.2.840.113549.1.1.12" => Some("sha384WithRSAEncryption"),
|
||||
"1.2.840.113549.1.1.13" => Some("sha512WithRSAEncryption"),
|
||||
"1.2.840.10045.2.1" => Some("ecPublicKey"),
|
||||
"1.2.840.10045.4.3.2" => Some("ecdsa-with-SHA256"),
|
||||
"1.2.840.10045.4.3.3" => Some("ecdsa-with-SHA384"),
|
||||
"1.2.840.10045.4.3.4" => Some("ecdsa-with-SHA512"),
|
||||
"1.3.101.112" => Some("Ed25519"),
|
||||
"1.3.101.113" => Some("Ed448"),
|
||||
"1.3.101.110" => Some("X25519"),
|
||||
"1.3.101.111" => Some("X448"),
|
||||
|
||||
// Extension OIDs (id-ce and id-pe)
|
||||
"2.5.29.14" => Some("subjectKeyIdentifier"),
|
||||
"2.5.29.15" => Some("keyUsage"),
|
||||
"2.5.29.17" => Some("subjectAltName"),
|
||||
"2.5.29.18" => Some("issuerAltName"),
|
||||
"2.5.29.19" => Some("basicConstraints"),
|
||||
"2.5.29.30" => Some("nameConstraints"),
|
||||
"2.5.29.31" => Some("cRLDistributionPoints"),
|
||||
"2.5.29.32" => Some("certificatePolicies"),
|
||||
"2.5.29.35" => Some("authorityKeyIdentifier"),
|
||||
"2.5.29.37" => Some("extKeyUsage"),
|
||||
"1.3.6.1.5.5.7.1.1" => Some("authorityInfoAccess"),
|
||||
"1.3.6.1.5.5.7.1.3" => Some("qcStatements"),
|
||||
|
||||
// AIA access methods
|
||||
"1.3.6.1.5.5.7.48.1" => Some("id-ad-ocsp"),
|
||||
"1.3.6.1.5.5.7.48.2" => Some("id-ad-caIssuers"),
|
||||
|
||||
// EKUs
|
||||
"1.3.6.1.5.5.7.3.1" => Some("serverAuth"),
|
||||
"1.3.6.1.5.5.7.3.2" => Some("clientAuth"),
|
||||
"1.3.6.1.5.5.7.3.3" => Some("codeSigning"),
|
||||
"1.3.6.1.5.5.7.3.4" => Some("emailProtection"),
|
||||
"1.3.6.1.5.5.7.3.8" => Some("timeStamping"),
|
||||
"1.3.6.1.5.5.7.3.9" => Some("OCSPSigning"),
|
||||
|
||||
// Microsoft EKUs
|
||||
"1.3.6.1.4.1.311.20.2.2" => Some("smartcardLogon"),
|
||||
|
||||
// Kerberos EKUs
|
||||
"1.3.6.1.5.2.3.5" => Some("kdcSigning"), _ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// removed unused type_name helper
|
||||
|
||||
fn tag_class_name(class: TagClass) -> &'static str {
|
||||
match class {
|
||||
TagClass::Universal => "UNIV ",
|
||||
TagClass::Application => "APPL ",
|
||||
TagClass::ContextSpecific => "CTX ",
|
||||
TagClass::Private => "PRIV ",
|
||||
}
|
||||
}
|
||||
|
||||
fn print_indent(indent: usize) { for _ in 0..indent { print!(" "); } }
|
||||
|
||||
fn human_utctime(s: &str) -> Option<String> {
|
||||
// RFC 5280: UTCTime YYMMDDHHMMSSZ or YYMMDDHHMMZ; years 1950-2049
|
||||
let z = s.ends_with('Z');
|
||||
if !z { return None; }
|
||||
let digits = &s[..s.len()-1];
|
||||
if digits.len() != 12 && digits.len() != 10 { return None; }
|
||||
let yy = digits.get(0..2)?.parse::<u32>().ok()?;
|
||||
let year = if yy >= 50 { 1900 + yy as i32 } else { 2000 + yy as i32 };
|
||||
let mon = digits.get(2..4)?.parse::<u32>().ok()?;
|
||||
let day = digits.get(4..6)?.parse::<u32>().ok()?;
|
||||
let hour = digits.get(6..8)?.parse::<u32>().ok()?;
|
||||
let min = digits.get(8..10)?.parse::<u32>().ok()?;
|
||||
let sec = if digits.len() == 12 { digits.get(10..12)?.parse::<u32>().ok()? } else { 0 };
|
||||
let date = NaiveDate::from_ymd_opt(year, mon, day)?;
|
||||
let dt = date.and_hms_opt(hour, min, sec)?;
|
||||
Some(format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC", dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second()))
|
||||
}
|
||||
|
||||
fn human_generalizedtime(s: &str) -> Option<String> {
|
||||
// GeneralizedTime: YYYYMMDDHHMMSSZ (common form)
|
||||
let z = s.ends_with('Z');
|
||||
if !z { return None; }
|
||||
let digits = &s[..s.len()-1];
|
||||
if digits.len() < 12 { return None; }
|
||||
let year = digits.get(0..4)?.parse::<i32>().ok()?;
|
||||
let mon = digits.get(4..6)?.parse::<u32>().ok()?;
|
||||
let day = digits.get(6..8)?.parse::<u32>().ok()?;
|
||||
let hour = digits.get(8..10)?.parse::<u32>().ok()?;
|
||||
let min = if digits.len() >= 12 { digits.get(10..12)?.parse::<u32>().ok()? } else { 0 };
|
||||
let mut sec = 0u32;
|
||||
if digits.len() >= 14 { sec = digits.get(12..14)?.parse::<u32>().ok()?; }
|
||||
let date = NaiveDate::from_ymd_opt(year, mon, day)?;
|
||||
let dt = date.and_hms_opt(hour, min, sec)?;
|
||||
Some(format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC", dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
use crate::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};
|
||||
|
||||
pub fn pretty_print_der(
|
||||
input: &[u8],
|
||||
color: bool,
|
||||
recursive: bool,
|
||||
indent: usize,
|
||||
parent_ctx: ParentCtx,
|
||||
bits_disp: BitsDisplay,
|
||||
) {
|
||||
let mut i = 0;
|
||||
let mut child_index: usize = 0;
|
||||
let mut last_child: Option<(TagClass, bool, u64, Vec<u8>)> = None;
|
||||
|
||||
while i < input.len() {
|
||||
let base = &input[i..];
|
||||
match parse_tlv(base) {
|
||||
Some(tlv) => {
|
||||
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);
|
||||
} else {
|
||||
let label = determine_label(&tlv, parent_ctx, child_index, &last_child);
|
||||
let ctx = PrintCtx {
|
||||
color,
|
||||
recursive,
|
||||
indent,
|
||||
bits_disp,
|
||||
};
|
||||
print_primitive_value_with_label(tlv, content, label, ctx);
|
||||
}
|
||||
|
||||
// Update state for next iteration
|
||||
last_child = Some((tlv.class, tlv.constructed, tlv.tag, content.to_vec()));
|
||||
i += tlv.total_len();
|
||||
child_index += 1;
|
||||
}
|
||||
None => {
|
||||
print_indent(indent);
|
||||
println!("{}", paint("<parse error>", MAGENTA, color));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn should_print_as_constructed(tlv: &TlvInfo) -> bool {
|
||||
tlv.constructed
|
||||
&& ((tlv.class == TagClass::Universal && (tlv.tag == 16 || tlv.tag == 17))
|
||||
|| tlv.class == TagClass::ContextSpecific)
|
||||
}
|
||||
|
||||
fn print_constructed_value(
|
||||
tlv: &TlvInfo,
|
||||
content: &[u8],
|
||||
color: bool,
|
||||
recursive: bool,
|
||||
indent: usize,
|
||||
bits_disp: BitsDisplay,
|
||||
) {
|
||||
print_indent(indent);
|
||||
|
||||
let (label, parent_ctx) = match (tlv.class, tlv.tag) {
|
||||
(TagClass::Universal, 16) => ("SEQUENCE", ParentCtx::Sequence),
|
||||
(TagClass::Universal, 17) => ("SET", ParentCtx::Set),
|
||||
(TagClass::ContextSpecific, tag) => {
|
||||
println!("[CTX {tag}] {{");
|
||||
pretty_print_der(content, color, recursive, indent + 1, ParentCtx::Ctx, bits_disp);
|
||||
print_indent(indent);
|
||||
println!("}}");
|
||||
return;
|
||||
}
|
||||
_ => unreachable!(), // should_print_as_constructed ensures this
|
||||
};
|
||||
|
||||
println!("{} {{", paint(label, BOLD, color));
|
||||
pretty_print_der(content, color, recursive, indent + 1, parent_ctx, bits_disp);
|
||||
print_indent(indent);
|
||||
println!("}}");
|
||||
}
|
||||
|
||||
fn determine_label<'a>(
|
||||
tlv: &TlvInfo,
|
||||
parent_ctx: ParentCtx,
|
||||
child_index: usize,
|
||||
last_child: &Option<(TagClass, bool, u64, Vec<u8>)>,
|
||||
) -> Option<&'a str> {
|
||||
match (tlv.class, tlv.tag) {
|
||||
(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_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::Sequence => {
|
||||
if child_index == 1 {
|
||||
if let Some((prev_class, prev_constructed, prev_tag, ref prev_content)) = last_child {
|
||||
if *prev_class == TagClass::Universal && *prev_constructed && *prev_tag == 16 {
|
||||
// Check if previous sequence contains RSA or EC public key OID
|
||||
if let Some(oid_tlv) = parse_tlv(prev_content) {
|
||||
if oid_tlv.class == TagClass::Universal && oid_tlv.tag == 6 {
|
||||
let oid = decode_oid(&prev_content[oid_tlv.content_start..oid_tlv.content_end()]);
|
||||
if oid == "1.2.840.113549.1.1.1" || oid == "1.2.840.10045.2.1" {
|
||||
return Some("subjectPublicKey");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_octet_string_label(
|
||||
last_child: &Option<(TagClass, bool, u64, Vec<u8>)>,
|
||||
) -> Option<&'static str> {
|
||||
if let Some((prev_class, _prev_constructed, prev_tag, ref prev_content)) = last_child {
|
||||
if *prev_class == TagClass::Universal && *prev_tag == 6 {
|
||||
let oid = decode_oid(prev_content);
|
||||
if oid == "2.5.29.15" {
|
||||
// keyUsage
|
||||
return Some("keyUsage");
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
use chrono::{Datelike, NaiveDate, Timelike};
|
||||
|
||||
pub fn human_utctime(s: &str) -> Option<String> {
|
||||
// RFC 5280: UTCTime YYMMDDHHMMSSZ or YYMMDDHHMMZ; years 1950-2049
|
||||
if !s.ends_with('Z') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let digits = &s[..s.len() - 1];
|
||||
if digits.len() != 12 && digits.len() != 10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let yy = digits.get(0..2)?.parse::<u32>().ok()?;
|
||||
let year = if yy >= 50 { 1900 + yy as i32 } else { 2000 + yy as i32 };
|
||||
let mon = digits.get(2..4)?.parse::<u32>().ok()?;
|
||||
let day = digits.get(4..6)?.parse::<u32>().ok()?;
|
||||
let hour = digits.get(6..8)?.parse::<u32>().ok()?;
|
||||
let min = digits.get(8..10)?.parse::<u32>().ok()?;
|
||||
let sec = if digits.len() == 12 {
|
||||
digits.get(10..12)?.parse::<u32>().ok()?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(year, mon, day)?;
|
||||
let dt = date.and_hms_opt(hour, min, sec)?;
|
||||
|
||||
Some(format!(
|
||||
"{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
|
||||
dt.year(),
|
||||
dt.month(),
|
||||
dt.day(),
|
||||
dt.hour(),
|
||||
dt.minute(),
|
||||
dt.second()
|
||||
))
|
||||
}
|
||||
|
||||
pub fn human_generalizedtime(s: &str) -> Option<String> {
|
||||
// GeneralizedTime: YYYYMMDDHHMMSSZ (common form)
|
||||
if !s.ends_with('Z') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let digits = &s[..s.len() - 1];
|
||||
if digits.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let year = digits.get(0..4)?.parse::<i32>().ok()?;
|
||||
let mon = digits.get(4..6)?.parse::<u32>().ok()?;
|
||||
let day = digits.get(6..8)?.parse::<u32>().ok()?;
|
||||
let hour = digits.get(8..10)?.parse::<u32>().ok()?;
|
||||
let min = if digits.len() >= 12 {
|
||||
digits.get(10..12)?.parse::<u32>().ok()?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut sec = 0u32;
|
||||
if digits.len() >= 14 {
|
||||
sec = digits.get(12..14)?.parse::<u32>().ok()?;
|
||||
}
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(year, mon, day)?;
|
||||
let dt = date.and_hms_opt(hour, min, sec)?;
|
||||
|
||||
Some(format!(
|
||||
"{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
|
||||
dt.year(),
|
||||
dt.month(),
|
||||
dt.day(),
|
||||
dt.hour(),
|
||||
dt.minute(),
|
||||
dt.second()
|
||||
))
|
||||
}
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
use crate::asn1_types::{as_i128, decode_oid, ip_to_string, TagClass, TlvInfo};
|
||||
use crate::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};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PrintCtx {
|
||||
pub color: bool,
|
||||
pub recursive: bool,
|
||||
pub indent: usize,
|
||||
pub bits_disp: BitsDisplay,
|
||||
}
|
||||
|
||||
pub fn print_indent(indent: usize) {
|
||||
for _ in 0..indent {
|
||||
print!(" ");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_primitive_value_with_label(
|
||||
tlv: TlvInfo,
|
||||
content: &[u8],
|
||||
label: Option<&str>,
|
||||
ctx: PrintCtx,
|
||||
) {
|
||||
print_indent(ctx.indent);
|
||||
|
||||
match (tlv.class, tlv.tag) {
|
||||
(TagClass::Universal, 1) => print_boolean(content, ctx.color),
|
||||
(TagClass::Universal, 2) => print_integer(content, 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),
|
||||
(TagClass::Universal, 6) => print_object_identifier(content, ctx.color),
|
||||
(TagClass::Universal, 10) => print_enumerated(content, ctx.color),
|
||||
(TagClass::Universal, 12) => print_utf8_string(content, ctx.color),
|
||||
(TagClass::Universal, 19) => print_printable_string(content, ctx.color),
|
||||
(TagClass::Universal, 22) => print_ia5_string(content, ctx.color),
|
||||
(TagClass::Universal, 23) => print_utc_time(content, ctx.color),
|
||||
(TagClass::Universal, 24) => print_generalized_time(content, ctx.color),
|
||||
(TagClass::ContextSpecific, 1) => print_rfc822_name(content),
|
||||
(TagClass::ContextSpecific, 2) => print_dns_name(content),
|
||||
(TagClass::ContextSpecific, 6) => print_uri(content),
|
||||
(TagClass::ContextSpecific, 7) => print_ip_address(content),
|
||||
_ => print_generic_value(tlv.class, tlv.tag, content),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_boolean(content: &[u8], color: bool) {
|
||||
let v = content.first().copied().unwrap_or(0) != 0;
|
||||
println!(
|
||||
"{} {}",
|
||||
paint("BOOLEAN", CYAN, color),
|
||||
paint(if v { "TRUE" } else { "FALSE" }, YELLOW, color)
|
||||
);
|
||||
}
|
||||
|
||||
fn print_integer(content: &[u8], color: bool) {
|
||||
if let Some(v) = as_i128(content) {
|
||||
println!(
|
||||
"{} {}",
|
||||
paint("INTEGER", CYAN, color),
|
||||
paint(&v.to_string(), YELLOW, color)
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{} 0x{}",
|
||||
paint("INTEGER", CYAN, color),
|
||||
paint(&to_hex_upper(content), YELLOW, color)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let unused = content.first().copied().unwrap_or(0) as usize;
|
||||
let bits = content.get(1..).unwrap_or(&[]);
|
||||
let title = if let Some(l) = label {
|
||||
format!("{} {}", l, "BIT STRING")
|
||||
} else {
|
||||
"BIT STRING".to_string()
|
||||
};
|
||||
|
||||
let total_bits = bits.len() * 8 - unused;
|
||||
let (pattern, truncated) = match ctx.bits_disp.format {
|
||||
BitsFormat::Hex => {
|
||||
let (hex, t) = bits_to_hex_truncated(bits, ctx.bits_disp.truncate_bytes);
|
||||
(format!("0x{hex}"), t)
|
||||
}
|
||||
BitsFormat::Auto | BitsFormat::Bits => {
|
||||
bits_to_string_truncated(bits, ctx.bits_disp.truncate_bits)
|
||||
}
|
||||
};
|
||||
|
||||
let pattern = if truncated {
|
||||
format!("{pattern}...")
|
||||
} else {
|
||||
pattern
|
||||
};
|
||||
|
||||
println!(
|
||||
"{} ({} bit) {}{}",
|
||||
paint(&title, CYAN, ctx.color),
|
||||
paint(&total_bits.to_string(), MAGENTA, ctx.color),
|
||||
paint(&pattern, YELLOW, ctx.color),
|
||||
if truncated { " (truncated)" } else { "" }
|
||||
);
|
||||
|
||||
if unused == 0 && looks_like_single_der(bits) {
|
||||
pretty_print_der(
|
||||
bits,
|
||||
ctx.color,
|
||||
ctx.recursive,
|
||||
ctx.indent + 1,
|
||||
ParentCtx::Sequence,
|
||||
ctx.bits_disp,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let title = if let Some(l) = label {
|
||||
format!("{} {}", l, "OCTET STRING")
|
||||
} else {
|
||||
"OCTET STRING".to_string()
|
||||
};
|
||||
|
||||
println!(
|
||||
"{} ({} byte) {}",
|
||||
paint(&title, CYAN, ctx.color),
|
||||
paint(&content.len().to_string(), MAGENTA, ctx.color),
|
||||
paint(&to_hex_upper(content), YELLOW, ctx.color)
|
||||
);
|
||||
|
||||
if looks_like_single_der(content) {
|
||||
pretty_print_der(
|
||||
content,
|
||||
ctx.color,
|
||||
ctx.recursive,
|
||||
ctx.indent + 1,
|
||||
ParentCtx::Sequence,
|
||||
ctx.bits_disp,
|
||||
);
|
||||
}
|
||||
|
||||
// Special-case decode for keyUsage wrapped in OCTET STRING
|
||||
if matches!(label, Some("keyUsage")) {
|
||||
if let Some(names) = decode_keyusage_from_octet(content) {
|
||||
print_indent(ctx.indent + 1);
|
||||
println!("keyUsage: {}", names.join(", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_null(color: bool) {
|
||||
println!("{}", paint("NULL", CYAN, color));
|
||||
}
|
||||
|
||||
fn print_object_identifier(content: &[u8], color: bool) {
|
||||
let oid = decode_oid(content);
|
||||
if let Some(name) = oid_friendly(&oid) {
|
||||
println!(
|
||||
"{} {} ({})",
|
||||
paint("OBJECTIDENTIFIER", CYAN, color),
|
||||
paint(&oid, YELLOW, color),
|
||||
name
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{} {}",
|
||||
paint("OBJECTIDENTIFIER", CYAN, color),
|
||||
paint(&oid, YELLOW, color)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_enumerated(content: &[u8], color: bool) {
|
||||
if let Some(v) = as_i128(content) {
|
||||
println!(
|
||||
"{} {}",
|
||||
paint("ENUMERATED", CYAN, color),
|
||||
paint(&v.to_string(), YELLOW, color)
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{} 0x{}",
|
||||
paint("ENUMERATED", CYAN, color),
|
||||
paint(&to_hex_upper(content), YELLOW, color)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_utf8_string(content: &[u8], color: bool) {
|
||||
match std::str::from_utf8(content) {
|
||||
Ok(s) => println!("{} '{}'", paint("UTF8String", CYAN, color), s),
|
||||
Err(_) => println!(
|
||||
"{} 0x{}",
|
||||
paint("UTF8String", CYAN, color),
|
||||
paint(&to_hex_upper(content), YELLOW, color)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_printable_string(content: &[u8], color: bool) {
|
||||
match std::str::from_utf8(content) {
|
||||
Ok(s) => println!("{} '{}'", paint("PrintableString", CYAN, color), s),
|
||||
Err(_) => println!(
|
||||
"{} 0x{}",
|
||||
paint("PrintableString", CYAN, color),
|
||||
paint(&to_hex_upper(content), YELLOW, color)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_ia5_string(content: &[u8], color: bool) {
|
||||
match std::str::from_utf8(content) {
|
||||
Ok(s) => println!("{} '{}'", paint("IA5String", CYAN, color), s),
|
||||
Err(_) => println!(
|
||||
"{} 0x{}",
|
||||
paint("IA5String", CYAN, color),
|
||||
paint(&to_hex_upper(content), YELLOW, color)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_utc_time(content: &[u8], color: bool) {
|
||||
match std::str::from_utf8(content) {
|
||||
Ok(s) => {
|
||||
if let Some(h) = human_utctime(s) {
|
||||
println!(
|
||||
"{} '{}' ({})",
|
||||
paint("UTCTime", CYAN, color),
|
||||
s,
|
||||
paint(&h, YELLOW, color)
|
||||
);
|
||||
} else {
|
||||
println!("{} '{}'", paint("UTCTime", CYAN, color), s);
|
||||
}
|
||||
}
|
||||
Err(_) => println!(
|
||||
"{} 0x{}",
|
||||
paint("UTCTime", CYAN, color),
|
||||
paint(&to_hex_upper(content), YELLOW, color)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_generalized_time(content: &[u8], color: bool) {
|
||||
match std::str::from_utf8(content) {
|
||||
Ok(s) => {
|
||||
if let Some(h) = human_generalizedtime(s) {
|
||||
println!(
|
||||
"{} '{}' ({})",
|
||||
paint("GeneralizedTime", CYAN, color),
|
||||
s,
|
||||
paint(&h, YELLOW, color)
|
||||
);
|
||||
} else {
|
||||
println!("{} '{}'", paint("GeneralizedTime", CYAN, color), s);
|
||||
}
|
||||
}
|
||||
Err(_) => println!(
|
||||
"{} 0x{}",
|
||||
paint("GeneralizedTime", CYAN, color),
|
||||
paint(&to_hex_upper(content), YELLOW, color)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_rfc822_name(content: &[u8]) {
|
||||
match std::str::from_utf8(content) {
|
||||
Ok(s) => println!("rfc822Name '{s}'"),
|
||||
Err(_) => println!("[CTX 1] 0x{}", to_hex_upper(content)),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_dns_name(content: &[u8]) {
|
||||
match std::str::from_utf8(content) {
|
||||
Ok(s) => println!("dNSName '{s}'"),
|
||||
Err(_) => println!("[CTX 2] 0x{}", to_hex_upper(content)),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_uri(content: &[u8]) {
|
||||
match std::str::from_utf8(content) {
|
||||
Ok(s) => println!("uniformResourceIdentifier '{s}'"),
|
||||
Err(_) => println!("[CTX 6] 0x{}", to_hex_upper(content)),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_ip_address(content: &[u8]) {
|
||||
if let Some(ip) = ip_to_string(content) {
|
||||
println!("iPAddress '{ip}'");
|
||||
} else {
|
||||
println!("[CTX 7] 0x{}", to_hex_upper(content));
|
||||
}
|
||||
}
|
||||
|
||||
fn print_generic_value(class: TagClass, tag: u64, content: &[u8]) {
|
||||
println!("[{}{}] 0x{}", class.name(), tag, to_hex_upper(content));
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ emailAddress = admin@example.com
|
|||
[ v3_req ]
|
||||
basicConstraints = CA:false
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = serverAuth, clientAuth, codeSigning, emailProtection, timeStamping
|
||||
extendedKeyUsage = serverAuth, clientAuth, codeSigning, emailProtection, timeStamping, OCSPSigning
|
||||
subjectAltName = @alt_names
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid
|
||||
|
|
|
|||
Loading…
Reference in New Issue