From bdce6f9810410c316925e378bfa9d793bb29a21f Mon Sep 17 00:00:00 2001 From: choksi2212 Date: Sat, 24 Oct 2026 09:26:26 +0530 Subject: [PATCH] pem-rfc7468: accept a lone CR before the pre-encapsulation boundary Section 3's grammar ends every line with eol = CRLF / CR / LF, and the eol helpers already accept a lone CR, but strip_preamble only looked for the boundary after an LF. A preamble ended by a lone CR (or the previous message's trailing eol in a multi-certificate chain) was refused as Error::Preamble even though the boundary follows it. Accept a CR followed by the boundary the same way the LF case is accepted. A CRLF is unaffected: the CR is then followed by the LF, not the boundary, so the walk still lands on the LF arm. Fixes #2486 Signed-off-by: choksi2212 --- pem-rfc7468/src/grammar.rs | 6 +++ pem-rfc7468/tests/decode.rs | 97 +++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/pem-rfc7468/src/grammar.rs b/pem-rfc7468/src/grammar.rs index b309371e7..82b041eaa 100644 --- a/pem-rfc7468/src/grammar.rs +++ b/pem-rfc7468/src/grammar.rs @@ -55,6 +55,9 @@ pub(crate) fn is_wsp(char: u8) -> bool { /// /// Returns a slice which starts at the beginning of the encapsulated text. /// +/// The preamble may be ended by any of the line endings Section 3 allows +/// (`eol = CRLF / CR / LF`); the boundary after it is what is looked for. +/// /// From RFC7468: /// > Data before the encapsulation boundaries are permitted, and /// > parsers MUST NOT malfunction when processing such data. @@ -71,6 +74,9 @@ pub(crate) fn strip_preamble(mut bytes: &[u8]) -> Result<&[u8]> { CHAR_LF if remaining.starts_with(PRE_ENCAPSULATION_BOUNDARY) => { return Ok(remaining); } + CHAR_CR if remaining.starts_with(PRE_ENCAPSULATION_BOUNDARY) => { + return Ok(remaining); + } _ => (), } diff --git a/pem-rfc7468/tests/decode.rs b/pem-rfc7468/tests/decode.rs index eb3a3201b..c79c662a2 100644 --- a/pem-rfc7468/tests/decode.rs +++ b/pem-rfc7468/tests/decode.rs @@ -125,3 +125,100 @@ fn line_width_detection() { 70 ); } + +/// RFC 7468 Section 3: `eol = CRLF / CR / LF`. A preamble line ended by a +/// lone CR must still find the pre-encapsulation boundary after it. +#[test] +fn preamble_ended_by_lone_cr() { + const PEM: &[u8] = include_bytes!("examples/pkcs1.pem"); + const DER: &[u8] = include_bytes!("examples/pkcs1.der"); + let mut input = [0u8; PEM.len() + 6]; + input[..5].copy_from_slice(b"chain"); + input[5] = 0x0D; + input[6..].copy_from_slice(PEM); + let mut buf = [0u8; 2048]; + let (label, decoded) = pem_rfc7468::decode(&input, &mut buf).unwrap(); + assert_eq!(label, "RSA PRIVATE KEY"); + assert_eq!(decoded, DER); +} + +/// The second message of a chain, as callers that split on the +/// post-encapsulation boundary hand it over: the previous message's trailing +/// lone CR in front, and every line end of the message itself a lone CR. +#[test] +fn lone_cr_line_endings_after_preceding_text() { + const PEM: &[u8] = include_bytes!("examples/pkcs1.pem"); + const DER: &[u8] = include_bytes!("examples/pkcs1.der"); + let mut input = [0u8; PEM.len() + 6]; + input[..5].copy_from_slice(b"chain"); + input[5] = 0x0D; + for (i, &byte) in PEM.iter().enumerate() { + input[6 + i] = if byte == 0x0A { 0x0D } else { byte }; + } + let mut buf = [0u8; 2048]; + let (label, decoded) = pem_rfc7468::decode(&input, &mut buf).unwrap(); + assert_eq!(label, "RSA PRIVATE KEY"); + assert_eq!(decoded, DER); +} + +/// A bare eol in front of the boundary, with no preamble text at all. +#[test] +fn boundary_after_lone_cr() { + const PEM: &[u8] = include_bytes!("examples/pkcs1.pem"); + let mut input = [0u8; PEM.len() + 1]; + input[0] = 0x0D; + input[1..].copy_from_slice(PEM); + assert_eq!( + pem_rfc7468::decode_label(&input).unwrap(), + "RSA PRIVATE KEY" + ); +} + +/// The CRLF path is unchanged: the CR arm must not consume a CR that an LF +/// follows, and the boundary after the LF is still found. +#[test] +fn preamble_ended_by_crlf() { + const PEM: &[u8] = include_bytes!("examples/pkcs1.pem"); + const DER: &[u8] = include_bytes!("examples/pkcs1.der"); + let mut input = [0u8; PEM.len() + 7]; + input[..5].copy_from_slice(b"chain"); + input[5] = 0x0D; + input[6] = 0x0A; + input[7..].copy_from_slice(PEM); + let mut buf = [0u8; 2048]; + let (label, decoded) = pem_rfc7468::decode(&input, &mut buf).unwrap(); + assert_eq!(label, "RSA PRIVATE KEY"); + assert_eq!(decoded, DER); +} + +/// The OpenSSL-inspired NUL refusal still applies to a preamble that never +/// reaches the boundary. +#[test] +fn preamble_with_nul_byte_is_still_refused() { + const PEM: &[u8] = include_bytes!("examples/pkcs1.pem"); + let mut input = [0u8; PEM.len() + 6]; + input[..3].copy_from_slice(b"cha"); + input[3] = 0x00; + input[4] = b'n'; + input[5] = 0x0D; + input[6..].copy_from_slice(PEM); + let mut buf = [0u8; 2048]; + let result = pem_rfc7468::decode(&input, &mut buf); + assert_eq!(result, Err(pem_rfc7468::Error::Preamble)); +} + +#[test] +#[cfg(feature = "alloc")] +fn lone_cr_chain_with_vec() { + const PEM: &[u8] = include_bytes!("examples/pkcs1.pem"); + const DER: &[u8] = include_bytes!("examples/pkcs1.der"); + let mut input = [0u8; PEM.len() + 6]; + input[..5].copy_from_slice(b"chain"); + input[5] = 0x0D; + for (i, &byte) in PEM.iter().enumerate() { + input[6 + i] = if byte == 0x0A { 0x0D } else { byte }; + } + let (label, decoded) = pem_rfc7468::decode_vec(&input).unwrap(); + assert_eq!(label, "RSA PRIVATE KEY"); + assert_eq!(decoded, DER); +}