From 802f9b829fbecbdb34a927f1e63ebd252460aa7b Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Sun, 28 Jul 2024 12:21:15 -0600 Subject: [PATCH] pem-rfc7468: add `detect_base64_line_width` and `new_detect_wrap` Adds the following new functions: - `detect_base64_line_width`: a toplevel free function similar to `decode_label` which outputs an autodetected line width for a given input PEM byte slice. - `Decoder::new_detect_wrap`: an alternative to `Decoder::new_wrapped` which attempts to autodetect the Base64 line width in order to flexibly handle inputs wrapped at any size. For an initial implementation, empty space between the pre-encapsulation boundary and the start of the Base64 is not handled. --- pem-rfc7468/src/decoder.rs | 27 +++++++++++++++++++ pem-rfc7468/src/lib.rs | 2 +- pem-rfc7468/tests/decode.rs | 15 +++++++++++ pem-rfc7468/tests/examples/ssh-id_ed25519.pem | 7 +++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 pem-rfc7468/tests/examples/ssh-id_ed25519.pem diff --git a/pem-rfc7468/src/decoder.rs b/pem-rfc7468/src/decoder.rs index 387c1e9f9..86f3d140f 100644 --- a/pem-rfc7468/src/decoder.rs +++ b/pem-rfc7468/src/decoder.rs @@ -62,6 +62,13 @@ pub fn decode_label(pem: &[u8]) -> Result<&str> { Ok(Encapsulation::try_from(pem)?.label()) } +/// Attempt to detect the Base64 line width for the given PEM document. +/// +/// NOTE: not constant time with respect to the input. +pub fn detect_base64_line_width(pem: &[u8]) -> Result { + Ok(Encapsulation::try_from(pem)?.encapsulated_text_line_width()) +} + /// Buffered PEM decoder. /// /// Stateful buffered decoder type which decodes an input PEM document according @@ -88,9 +95,19 @@ impl<'i> Decoder<'i> { let encapsulation = Encapsulation::try_from(pem)?; let type_label = encapsulation.label(); let base64 = Base64Decoder::new_wrapped(encapsulation.encapsulated_text, line_width)?; + Ok(Self { type_label, base64 }) } + /// Create a new PEM [`Decoder`] which automatically detects the line width the input is wrapped + /// at and flexibly handles widths other than the default 64-characters. + /// + /// Note: unlike `new` and `new_wrapped`, this method is not constant-time. + pub fn new_detect_wrap(pem: &'i [u8]) -> Result { + let line_width = detect_base64_line_width(pem)?; + Self::new_wrapped(pem, line_width) + } + /// Get the PEM type label for the input document. pub fn type_label(&self) -> &'i str { self.type_label @@ -224,6 +241,16 @@ impl<'a> Encapsulation<'a> { pub fn label(self) -> &'a str { self.label } + + /// Detect the line width of the encapsulated text by looking for the position of the first EOL. + pub fn encapsulated_text_line_width(self) -> usize { + // TODO(tarcieri): handle empty space between the pre-encapsulation boundary and Base64 + self.encapsulated_text + .iter() + .copied() + .position(|c| matches!(c, grammar::CHAR_CR | grammar::CHAR_LF)) + .unwrap_or(self.encapsulated_text.len()) + } } impl<'a> TryFrom<&'a [u8]> for Encapsulation<'a> { diff --git a/pem-rfc7468/src/lib.rs b/pem-rfc7468/src/lib.rs index 60ad97c67..25c406232 100644 --- a/pem-rfc7468/src/lib.rs +++ b/pem-rfc7468/src/lib.rs @@ -63,7 +63,7 @@ mod error; mod grammar; pub use crate::{ - decoder::{decode, decode_label, Decoder}, + decoder::{decode, decode_label, detect_base64_line_width, Decoder}, encoder::{encapsulated_len, encapsulated_len_wrapped, encode, encoded_len, Encoder}, error::{Error, Result}, }; diff --git a/pem-rfc7468/tests/decode.rs b/pem-rfc7468/tests/decode.rs index dc51528b3..93e2f07e3 100644 --- a/pem-rfc7468/tests/decode.rs +++ b/pem-rfc7468/tests/decode.rs @@ -110,3 +110,18 @@ fn ed25519_example() { let label = pem_rfc7468::decode_label(pem).unwrap(); assert_eq!(label, "ED25519 CERT"); } + +#[test] +fn line_width_detection() { + let pem_64cols = include_bytes!("examples/pkcs1.pem"); + assert_eq!( + pem_rfc7468::detect_base64_line_width(pem_64cols).unwrap(), + 64 + ); + + let pem_70cols = include_bytes!("examples/ssh-id_ed25519.pem"); + assert_eq!( + pem_rfc7468::detect_base64_line_width(pem_70cols).unwrap(), + 70 + ); +} diff --git a/pem-rfc7468/tests/examples/ssh-id_ed25519.pem b/pem-rfc7468/tests/examples/ssh-id_ed25519.pem new file mode 100644 index 000000000..5c4e7b10e --- /dev/null +++ b/pem-rfc7468/tests/examples/ssh-id_ed25519.pem @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYgAAAJgAIAxdACAM +XQAAAAtzc2gtZWQyNTUxOQAAACCzPq7zfqLffKoBDe/eo04kH2XxtSmk9D7RQyf1xUqrYg +AAAEC2BsIi0QwW2uFscKTUUXNHLsYX4FxlaSDSblbAj7WR7bM+rvN+ot98qgEN796jTiQf +ZfG1KaT0PtFDJ/XFSqtiAAAAEHVzZXJAZXhhbXBsZS5jb20BAgMEBQ== +-----END OPENSSH PRIVATE KEY-----