From 19b03c3c8d692d243c75e2daee3effd99c69c08f Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 1 Aug 2024 16:52:03 -0400 Subject: [PATCH] Added an asn1parse command for local testing --- .github/workflows/ci.yml | 7 +- Cargo.toml | 4 +- README.md | 8 +- asn1_derive/Cargo.toml | 2 +- asn1parse/Cargo.toml | 11 +++ asn1parse/src/lib.rs | 156 +++++++++++++++++++++++++++++++++++++++ asn1parse/src/main.rs | 33 +++++++++ src/tag.rs | 8 +- 8 files changed, 216 insertions(+), 13 deletions(-) create mode 100644 asn1parse/Cargo.toml create mode 100644 asn1parse/src/lib.rs create mode 100644 asn1parse/src/main.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18d61ed..2ce6ff7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: matrix: RUST: # MSRV - - VERSION: "1.59.0" + - VERSION: "1.65.0" FLAGS: "" - VERSION: stable @@ -88,8 +88,9 @@ jobs: - run: | cargo generate-lockfile - cargo update -p libc --precise 0.2.163 - if: matrix.RUST.VERSION == '1.59.0' + cargo update -p anstyle --precise 1.0.8 + cargo update -p clap_builder --precise 4.3.24 + if: matrix.RUST.VERSION == '1.65.0' - run: cargo check --workspace --tests ${{ matrix.RUST.FLAGS }} - run: cargo test --workspace ${{ matrix.RUST.FLAGS }} diff --git a/Cargo.toml b/Cargo.toml index b109afe..218fb74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "BSD-3-Clause" description = "ASN.1 (DER) parser and writer for Rust." edition = "2021" # This specifies the MSRV -rust-version = "1.59.0" +rust-version = "1.65.0" [features] default = ["std"] @@ -22,4 +22,4 @@ itoa = "1.0.11" libc = "0.2.11" [workspace] -members = ["asn1_derive"] +members = ["asn1_derive", "asn1parse"] diff --git a/README.md b/README.md index 4f39713..16cf541 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Add `asn1` to the `[dependencies]` section of your `Cargo.toml`: asn1 = "0.20" ``` -Builds on Rust 1.59.0 and newer. +Builds on Rust 1.65.0 and newer. `rust-asn1` is compatible with `#![no_std]` environments: @@ -24,7 +24,11 @@ asn1 = { version = "0.20", default-features = false } ## Changelog -### [0.20.1] +### [0.21.0] + +#### Changes + +- Updated MSRV to 1.65.0. #### Fixes diff --git a/asn1_derive/Cargo.toml b/asn1_derive/Cargo.toml index efe2538..4211fb2 100644 --- a/asn1_derive/Cargo.toml +++ b/asn1_derive/Cargo.toml @@ -7,7 +7,7 @@ license = "BSD-3-Clause" description = "#[derive] support for asn1" edition = "2021" # This specifies the MSRV -rust-version = "1.59.0" +rust-version = "1.65.0" [lib] proc-macro = true diff --git a/asn1parse/Cargo.toml b/asn1parse/Cargo.toml new file mode 100644 index 0000000..14f8b3d --- /dev/null +++ b/asn1parse/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "asn1parse" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +asn1 = { version = "0.20", path = ".." } +clap = { version = "4.3", features = ["derive"] } +hex = "0.4.3" +pem = { version = "3.0.4", default-features = false, features = ["std"] } diff --git a/asn1parse/src/lib.rs b/asn1parse/src/lib.rs new file mode 100644 index 0000000..5b3dfe2 --- /dev/null +++ b/asn1parse/src/lib.rs @@ -0,0 +1,156 @@ +#[derive(asn1::Asn1Read)] +pub enum Value<'a> { + SequenceOf(asn1::SequenceOf<'a, Value<'a>>), + SetOf(asn1::SetOf<'a, Value<'a>>), + Integer(asn1::BigInt<'a>), + Boolean(bool), + ObjectIdentifier(asn1::ObjectIdentifier), + Null(asn1::Null), + PrintableString(asn1::PrintableString<'a>), + IA5String(asn1::IA5String<'a>), + Utf8String(asn1::Utf8String<'a>), + OctetString(&'a [u8]), + BitString(asn1::BitString<'a>), + UtcTime(asn1::UtcTime), + + Fallback(asn1::Tlv<'a>), +} + +impl Value<'_> { + pub fn render( + &self, + out: &mut dyn std::io::Write, + indent: usize, + ) -> Result<(), Box> { + match self { + Value::SequenceOf(v) => { + writeln!(out, "{}SEQUENCE", " ".repeat(indent))?; + for el in v.clone() { + el.render(out, indent + 2)?; + } + } + Value::SetOf(v) => { + writeln!(out, "{}SET", " ".repeat(indent))?; + for el in v.clone() { + el.render(out, indent + 2)?; + } + } + Value::Integer(v) => { + let data = hex::encode(v.as_bytes()); + let mut output = data.trim_start_matches('0'); + if output.is_empty() { + output = "0"; + } + + writeln!( + out, + "{}INTEGER: {}0x{}", + " ".repeat(indent), + if v.is_negative() { "-" } else { "" }, + output, + )?; + } + Value::Boolean(v) => { + writeln!(out, "{}BOOLEAN: {:?}", " ".repeat(indent), v)?; + } + Value::ObjectIdentifier(v) => { + writeln!(out, "{}OBJECT IDENTIFIER: {}", " ".repeat(indent), v)?; + } + Value::PrintableString(v) => { + writeln!( + out, + "{}PRINTABLE STRING: {:?}", + " ".repeat(indent), + v.as_str() + )?; + } + Value::IA5String(v) => { + writeln!(out, "{}IA5 STRING: {:?}", " ".repeat(indent), v.as_str())?; + } + Value::Utf8String(v) => { + writeln!(out, "{}UTF8 STRING: {:?}", " ".repeat(indent), v.as_str())?; + } + Value::OctetString(v) => { + if let Ok(v) = asn1::parse_single::>(v) { + writeln!(out, "{}OCTET STRING", " ".repeat(indent))?; + v.render(out, indent + 2)?; + } else { + writeln!(out, "{}OCTET STRING: {:?}", " ".repeat(indent), v)?; + } + } + Value::BitString(v) => { + writeln!( + out, + "{}BIT STRING: {:?}", + " ".repeat(indent), + hex::encode(v.as_bytes()) + )?; + } + Value::UtcTime(v) => { + let dt = v.as_datetime(); + writeln!( + out, + "{}UTC TIME: {}-{:02}-{:02} {:02}:{:02}:{:02}", + " ".repeat(indent), + dt.year(), + dt.month(), + dt.day(), + dt.hour(), + dt.minute(), + dt.second() + )?; + } + Value::Null(_) => { + writeln!(out, "{}NULL", " ".repeat(indent))?; + } + Value::Fallback(tlv) => { + let tag = tlv.tag(); + if tag.is_constructed() { + writeln!( + out, + "{}[{:?} {}]", + " ".repeat(indent), + tag.class(), + tag.value() + )?; + asn1::parse_single::>(tlv.data())?.render(out, indent + 2)?; + } else { + writeln!( + out, + "{}[{:?} {} PRIMITIVE]: {:?}", + " ".repeat(indent), + tag.class(), + tag.value(), + tlv.data() + )?; + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::Value; + + #[test] + fn test_render() { + for (der, expected) in [ + (b"\x01\x01\xff" as &[u8], "BOOLEAN: true\n"), + (b"\x17\x0d910506234540Z", "UTC TIME: 1991-05-06 23:45:40\n"), + (b"\x13\x03abc", "PRINTABLE STRING: \"abc\"\n"), + (b"\x16\x03abc", "IA5 STRING: \"abc\"\n"), + (b"\x03\x03\x04\x81\xf0", "BIT STRING: \"81f0\"\n"), + (b"\x02\x01\x00", "INTEGER: 0x0\n"), + (b"\x02\x01\x02", "INTEGER: 0x2\n"), + (b"\x02\x01\x80", "INTEGER: -0x80\n"), + ] { + let v = asn1::parse_single::>(der).unwrap(); + let mut output = vec![]; + v.render(&mut output, 0).unwrap(); + assert_eq!(std::str::from_utf8(&output).unwrap(), expected); + } + } +} diff --git a/asn1parse/src/main.rs b/asn1parse/src/main.rs new file mode 100644 index 0000000..19bdffd --- /dev/null +++ b/asn1parse/src/main.rs @@ -0,0 +1,33 @@ +use clap::Parser; +use std::io::Read; +use std::path::PathBuf; + +#[derive(clap::Parser)] +struct Args { + #[clap(long)] + pem: bool, + + #[clap()] + path: Option, +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + + let mut data = if let Some(path) = args.path { + std::fs::read(path)? + } else { + let mut buf = vec![]; + std::io::stdin().read_to_end(&mut buf)?; + buf + }; + + if args.pem { + data = pem::parse(data)?.into_contents(); + } + + let v = asn1::parse_single::>(&data)?; + v.render(&mut std::io::stdout().lock(), 0)?; + + Ok(()) +} diff --git a/src/tag.rs b/src/tag.rs index 6a30ac8..4a9d317 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -45,19 +45,17 @@ impl Tag { // Long form tag if value == 0x1f { - let result = base128::read_base128_int(data).map_err(|e| { + let large_value; + (large_value, data) = base128::read_base128_int(data).map_err(|e| { if matches!(e.kind(), ParseErrorKind::ShortData { .. }) { e } else { ParseError::new(ParseErrorKind::InvalidTag) } })?; - // MSRV of 1.59 required for `(value, data) = ...;` - value = result - .0 + value = large_value .try_into() .map_err(|_| ParseError::new(ParseErrorKind::InvalidTag))?; - data = result.1; // Tags must be encoded in minimal form. if value < 0x1f { return Err(ParseError::new(ParseErrorKind::InvalidTag));