From 94a003f76df52344024b4e357d87a75857ff4688 Mon Sep 17 00:00:00 2001 From: jean-airoldie <25088801+jean-airoldie@users.noreply.github.com> Date: Sun, 4 May 2025 08:55:34 -0400 Subject: [PATCH] Add value field to ParsedExtension::UnsupportedExtension * This is a breaking change. * Allows a user to manually parse the value of a custom extension. * Add tests. * Fix clippy & run rustfmt --- .gitignore | 1 + assets/csr-custom-extension.pem | 7 +++++ src/extensions/mod.rs | 49 +++++++++++++++++++-------------- tests/readcsr.rs | 48 ++++++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 assets/csr-custom-extension.pem diff --git a/.gitignore b/.gitignore index a1d9281..7baa0e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .*.swp target /.idea +tags diff --git a/assets/csr-custom-extension.pem b/assets/csr-custom-extension.pem new file mode 100644 index 0000000..fceb6c7 --- /dev/null +++ b/assets/csr-custom-extension.pem @@ -0,0 +1,7 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIHJMH0CAQAwLjEbMBkGA1UEAwwSY2xpZW50IGNlcnRpZmljYXRlMQ8wDQYDVQQK +DAZteSBvcmcwKjAFBgMrZXADIQC3Gw0vJiIXML8uYH86NEMtmiMuAdWn2WPEZVKu +mbsOTaAcMBoGCSqGSIb3DQEJDjENMAswCQYCKgMEAwECAzAFBgMrZXADQQDPcUvh +yrGGTBlgv4W3//5/h5AkEG2UVa1Bvfbi/UL4tkFYhkc5sAFZ37Igr2tgDNhofu7r +JVo5qjVH6T9QJ8gD +-----END CERTIFICATE REQUEST----- diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index da74142..ff730c4 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -175,7 +175,7 @@ impl<'a> DerParser<'a> for X509Extension<'a> { let (rem, (_, value)) = <&[u8]>::parse_der_as_input(rem) .map_err(|_| Err::Error(X509Error::InvalidExtensions))?; - let (_, parsed_extension) = parser::parse_extension(value.clone(), &oid)?; + let parsed_extension = parser::parse_extension(value.clone(), &oid, critical); let ext = X509Extension { oid, critical, @@ -226,10 +226,11 @@ impl<'i> Parser> for X509ExtensionParser { let (rem, (_, value)) = <&[u8]>::parse_der_as_input(rem) .map_err(|_| Err::Error(X509Error::InvalidExtensions))?; - let (_, parsed_extension) = if self.deep_parse_extensions { - parser::parse_extension(value.clone(), &oid)? + let parsed_extension = if self.deep_parse_extensions { + parser::parse_extension(value.clone(), &oid, critical) } else { - (rem.take(rem.input_len()), ParsedExtension::Unparsed) + rem.take(rem.input_len()); + ParsedExtension::Unparsed }; let ext = X509Extension { @@ -258,12 +259,21 @@ impl<'i> Parser> for X509ExtensionParser { } } +/// A unsupported extension. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct UnsupportedExtension<'a> { + /// The Object ID of the extension. + pub oid: Oid<'a>, + /// The unparsed value. + pub value: &'a [u8], + /// Whether the extension is critical. + pub critical: bool, +} + #[derive(Clone, Debug, PartialEq)] pub enum ParsedExtension<'a> { /// Crate parser does not support this extension (yet) - UnsupportedExtension { - oid: Oid<'a>, - }, + UnsupportedExtension(UnsupportedExtension<'a>), ParseError { error: Err, }, @@ -422,30 +432,27 @@ pub(crate) mod parser { // look into the parser map if the extension is known, and parse it // otherwise, leave it as UnsupportedExtension - fn parse_extension0<'i>( - input: Input<'i>, - oid: &Oid, - ) -> IResult, ParsedExtension<'i>, X509Error> { + fn parse_extension0<'i>(input: Input<'i>, oid: &Oid, critical: bool) -> ParsedExtension<'i> { if let Some(parser) = EXTENSION_PARSERS.get(oid) { match parser(input.clone()) { - Ok((rem, ext)) => Ok((rem, ext)), - Err(error) => Ok((input, ParsedExtension::ParseError { error })), + Ok((_, ext)) => ext, + Err(error) => ParsedExtension::ParseError { error }, } } else { - Ok(( - input, - ParsedExtension::UnsupportedExtension { - oid: oid.to_owned(), - }, - )) + ParsedExtension::UnsupportedExtension(UnsupportedExtension { + oid: oid.to_owned(), + value: input.as_bytes2(), + critical, + }) } } pub(crate) fn parse_extension<'i>( input: Input<'i>, oid: &Oid, - ) -> IResult, ParsedExtension<'i>, X509Error> { - parse_extension0(input, oid) + critical: bool, + ) -> ParsedExtension<'i> { + parse_extension0(input, oid, critical) } fn parse_basicconstraints_ext(input: Input) -> IResult { diff --git a/tests/readcsr.rs b/tests/readcsr.rs index cc5a73a..998b0e9 100644 --- a/tests/readcsr.rs +++ b/tests/readcsr.rs @@ -1,13 +1,16 @@ -use asn1_rs::Set; +use asn1_rs::{oid, Oid, Set}; use oid_registry::{ - OID_PKCS1_SHA256WITHRSA, OID_PKCS9_CHALLENGE_PASSWORD, OID_SIG_ECDSA_WITH_SHA256, - OID_X509_COMMON_NAME, + OID_PKCS1_SHA256WITHRSA, OID_PKCS9_CHALLENGE_PASSWORD, OID_PKCS9_EXTENSION_REQUEST, + OID_SIG_ECDSA_WITH_SHA256, OID_X509_COMMON_NAME, }; use x509_parser::prelude::*; const CSR_DATA_EMPTY_ATTRIB: &[u8] = include_bytes!("../assets/csr-empty-attributes.csr"); const CSR_DATA: &[u8] = include_bytes!("../assets/test.csr"); const CSR_CHALLENGE_PASSWORD: &[u8] = include_bytes!("../assets/csr-challenge-password.pem"); +const CSR_CUSTOM_EXTENSION: &[u8] = include_bytes!("../assets/csr-custom-extension.pem"); +const OID_CUSTOM_EXTENSION: Oid<'static> = oid!(1.2.3); +const VALUE_CUSTOM_EXTENSION: &[u8] = &[1, 2, 3]; #[test] fn read_csr_empty_attrib() { let (rem, csr) = @@ -129,3 +132,42 @@ fn read_csr_verify() { let (_, csr) = X509CertificationRequest::from_der(&der.contents).expect("could not parse CSR"); csr.verify_signature().unwrap_err(); } + +#[test] +fn read_csr_with_custom_extension() { + let der = pem::parse_x509_pem(CSR_CUSTOM_EXTENSION).unwrap().1; + let (rem, csr) = X509CertificationRequest::from_der(&der.contents) + .expect("Could not parse CSR with custom extension"); + + assert!(rem.is_empty()); + dbg!(csr.certification_request_info.attributes()); + let cri = &csr.certification_request_info; + assert_eq!(cri.version, X509Version(0)); + assert_eq!(cri.attributes().len(), 1); + + let custom_attr = csr + .certification_request_info + .find_attribute(&OID_PKCS9_EXTENSION_REQUEST) + .expect("Custom extension not found in CSR"); + for attr in custom_attr.parsed_attributes() { + match attr { + ParsedCriAttribute::ExtensionRequest(req) => { + assert_eq!(req.extensions.len(), 1); + let extension = req.extensions.first().unwrap(); + assert_eq!(extension.oid, OID_CUSTOM_EXTENSION); + assert_eq!(extension.critical, false); + assert_eq!(extension.value.as_bytes2(), VALUE_CUSTOM_EXTENSION); + } + _ => unreachable!(), + } + } + + let extensions = csr.requested_extensions(); + for extension in extensions { + if let ParsedExtension::UnsupportedExtension(ext) = extension { + assert_eq!(ext.oid, OID_CUSTOM_EXTENSION); + assert_eq!(ext.value, VALUE_CUSTOM_EXTENSION); + assert_eq!(ext.critical, false); + } + } +}