diff --git a/Cargo.lock b/Cargo.lock index fe9703c83..bf8dee19c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,7 +431,7 @@ dependencies = [ "der_derive", "flagset", "hex-literal", - "pem-rfc7468 0.5.1", + "pem-rfc7468 0.6.0-pre", "proptest", "time", "zeroize", @@ -841,7 +841,7 @@ dependencies = [ [[package]] name = "pem-rfc7468" -version = "0.5.1" +version = "0.6.0-pre" dependencies = [ "base64ct 1.5.0", ] @@ -1430,7 +1430,7 @@ dependencies = [ "ed25519-dalek", "hex-literal", "p256", - "pem-rfc7468 0.5.1", + "pem-rfc7468 0.6.0-pre", "rand_chacha", "rand_core 0.6.3", "rsa", diff --git a/der/Cargo.toml b/der/Cargo.toml index 61224c0e1..332265046 100644 --- a/der/Cargo.toml +++ b/der/Cargo.toml @@ -19,7 +19,7 @@ rust-version = "1.57" const-oid = { version = "0.9", optional = true, path = "../const-oid" } der_derive = { version = "=0.6.0-pre.3", optional = true, path = "derive" } flagset = { version = "0.4.3", optional = true } -pem-rfc7468 = { version = "0.5", optional = true, path = "../pem-rfc7468" } +pem-rfc7468 = { version = "=0.6.0-pre", optional = true, path = "../pem-rfc7468" } time = { version = "0.3.4", optional = true, default-features = false } zeroize = { version = "1.5", optional = true, default-features = false, features = ["alloc"] } diff --git a/der/src/encode.rs b/der/src/encode.rs index 078026a8d..e092d3e38 100644 --- a/der/src/encode.rs +++ b/der/src/encode.rs @@ -98,22 +98,9 @@ pub trait EncodePem: Encode + PemLabel { #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] impl EncodePem for T { - #[allow(clippy::integer_arithmetic)] fn to_pem(&self, line_ending: LineEnding) -> Result { - // TODO(tarcieri): checked arithmetic, maybe extract this into `base64ct::base64_len`? let der_len = usize::try_from(self.encoded_len()?)?; - let mut base64_len = (((der_len * 4) / 3) + 3) & !3; - - // Add the length of the line endings which will be inserted when - // encoded Base64 is line wrapped - // TODO(tarcieri): factor this logic into `pem-rfc7468` - base64_len += base64_len - .saturating_sub(1) - .checked_div(64) - .and_then(|len| len.checked_add(line_ending.len())) - .ok_or(ErrorKind::Overflow)?; - - let pem_len = pem::encapsulated_len(Self::PEM_LABEL, line_ending, base64_len)?; + let pem_len = pem::encapsulated_len(Self::PEM_LABEL, line_ending, der_len)?; let mut buf = vec![0u8; pem_len]; let mut writer = PemWriter::new(Self::PEM_LABEL, line_ending, &mut buf)?; diff --git a/pem-rfc7468/Cargo.toml b/pem-rfc7468/Cargo.toml index 2c8330af1..b0481781c 100644 --- a/pem-rfc7468/Cargo.toml +++ b/pem-rfc7468/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pem-rfc7468" -version = "0.5.1" +version = "0.6.0-pre" description = """ PEM Encoding (RFC 7468) for PKIX, PKCS, and CMS Structures, implementing a strict subset of the original Privacy-Enhanced Mail encoding intended diff --git a/pem-rfc7468/src/encoder.rs b/pem-rfc7468/src/encoder.rs index 469661900..016e224c6 100644 --- a/pem-rfc7468/src/encoder.rs +++ b/pem-rfc7468/src/encoder.rs @@ -14,49 +14,73 @@ use alloc::string::String; use std::io; /// Compute the length of a PEM encoded document which encapsulates a -/// Base64-encoded body of the given length. +/// Base64-encoded body including line endings every 64 characters. /// -/// The `base64_len` value does *NOT* include the trailing newline's length. +/// The `input_len` parameter specifies the length of the raw input +/// bytes prior to Base64 encoding. +/// +/// Note that the current implementation of this function computes an upper +/// bound of the length and the actual encoded document may be slightly shorter +/// (typically 1-byte). Downstream consumers of this function should check the +/// actual encoded length and potentially truncate buffers allocated using this +/// function to estimate the encapsulated size. +/// +/// Use [`encoded_len`] (when possible) to obtain a precise length. /// /// ## Returns /// - `Ok(len)` on success /// - `Err(Error::Length)` on length overflow -pub fn encapsulated_len(label: &str, line_ending: LineEnding, base64_len: usize) -> Result { - [ - PRE_ENCAPSULATION_BOUNDARY.len(), - label.as_bytes().len(), - ENCAPSULATION_BOUNDARY_DELIMITER.len(), - line_ending.len(), - base64_len, - line_ending.len(), - POST_ENCAPSULATION_BOUNDARY.len(), - label.as_bytes().len(), - ENCAPSULATION_BOUNDARY_DELIMITER.len(), - line_ending.len(), - ] - .into_iter() - .try_fold(0usize, |acc, len| acc.checked_add(len)) - .ok_or(Error::Length) +pub fn encapsulated_len(label: &str, line_ending: LineEnding, input_len: usize) -> Result { + encapsulated_len_wrapped(label, BASE64_WRAP_WIDTH, line_ending, input_len) +} + +/// Compute the length of a PEM encoded document with the Base64 body +/// line wrapped at the specified `width`. +/// +/// This is the same as [`encapsulated_len`], which defaults to a width of 64. +/// +/// Note that per [RFC7468 § 2] encoding PEM with any other wrap width besides +/// 64 is technically non-compliant: +/// +/// > Generators MUST wrap the base64-encoded lines so that each line +/// > consists of exactly 64 characters except for the final line, which +/// > will encode the remainder of the data (within the 64-character line +/// > boundary) +/// +/// [RFC7468 § 2]: https://datatracker.ietf.org/doc/html/rfc7468#section-2 +pub fn encapsulated_len_wrapped( + label: &str, + line_width: usize, + line_ending: LineEnding, + input_len: usize, +) -> Result { + if line_width < 4 { + return Err(Error::Length); + } + + let base64_len = input_len + .checked_mul(4) + .and_then(|n| n.checked_div(3)) + .and_then(|n| n.checked_add(3)) + .ok_or(Error::Length)? + & !3; + + let base64_len_wrapped = base64_len_wrapped(base64_len, line_width, line_ending)?; + encapsulated_len_inner(label, line_ending, base64_len_wrapped) } /// Get the length of a PEM encoded document with the given bytes and label. /// +/// This function computes a precise length of the PEM encoding of the given +/// `input` data. +/// /// ## Returns /// - `Ok(len)` on success /// - `Err(Error::Length)` on length overflow pub fn encoded_len(label: &str, line_ending: LineEnding, input: &[u8]) -> Result { let base64_len = Base64::encoded_len(input); - - encapsulated_len( - label, - line_ending, - base64_len - .saturating_sub(1) - .checked_div(BASE64_WRAP_WIDTH) - .and_then(|len| len.checked_mul(line_ending.len())) - .and_then(|len| len.checked_add(base64_len)) - .ok_or(Error::Length)?, - ) + let base64_len_wrapped = base64_len_wrapped(base64_len, BASE64_WRAP_WIDTH, line_ending)?; + encapsulated_len_inner(label, line_ending, base64_len_wrapped) } /// Encode a PEM document according to RFC 7468's "Strict" grammar. @@ -104,6 +128,44 @@ pub fn encode_string(label: &str, line_ending: LineEnding, input: &[u8]) -> Resu String::from_utf8(buf).map_err(|_| Error::CharacterEncoding) } +/// Compute the encapsulated length of Base64 data of the given length. +fn encapsulated_len_inner( + label: &str, + line_ending: LineEnding, + base64_len: usize, +) -> Result { + [ + PRE_ENCAPSULATION_BOUNDARY.len(), + label.as_bytes().len(), + ENCAPSULATION_BOUNDARY_DELIMITER.len(), + line_ending.len(), + base64_len, + line_ending.len(), + POST_ENCAPSULATION_BOUNDARY.len(), + label.as_bytes().len(), + ENCAPSULATION_BOUNDARY_DELIMITER.len(), + line_ending.len(), + ] + .into_iter() + .try_fold(0usize, |acc, len| acc.checked_add(len)) + .ok_or(Error::Length) +} + +/// Compute Base64 length line-wrapped at the specified width with the given +/// line ending. +fn base64_len_wrapped( + base64_len: usize, + line_width: usize, + line_ending: LineEnding, +) -> Result { + base64_len + .saturating_sub(1) + .checked_div(line_width) + .and_then(|lines| lines.checked_mul(line_ending.len())) + .and_then(|len| len.checked_add(base64_len)) + .ok_or(Error::Length) +} + /// Buffered PEM encoder. /// /// Stateful buffered encoder type which encodes an input PEM document according @@ -129,6 +191,19 @@ impl<'l, 'o> Encoder<'l, 'o> { } /// Create a new PEM [`Encoder`] which wraps at the given line width. + /// + /// Note that per [RFC7468 § 2] encoding PEM with any other wrap width besides + /// 64 is technically non-compliant: + /// + /// > Generators MUST wrap the base64-encoded lines so that each line + /// > consists of exactly 64 characters except for the final line, which + /// > will encode the remainder of the data (within the 64-character line + /// > boundary) + /// + /// This method is provided with the intended purpose of implementing the + /// OpenSSH private key format, which uses a non-standard wrap width of 70. + /// + /// [RFC7468 § 2]: https://datatracker.ietf.org/doc/html/rfc7468#section-2 pub fn new_wrapped( type_label: &'l str, line_width: usize, @@ -207,7 +282,7 @@ impl<'l, 'o> Encoder<'l, 'o> { part.copy_from_slice(boundary_part); } - encapsulated_len(self.type_label, self.line_ending, base64.len()) + encapsulated_len_inner(self.type_label, self.line_ending, base64.len()) } } diff --git a/pem-rfc7468/src/lib.rs b/pem-rfc7468/src/lib.rs index c379f38b7..15da39eed 100644 --- a/pem-rfc7468/src/lib.rs +++ b/pem-rfc7468/src/lib.rs @@ -62,7 +62,7 @@ mod grammar; pub use crate::{ decoder::{decode, decode_label, Decoder}, - encoder::{encapsulated_len, encode, encoded_len, Encoder}, + encoder::{encapsulated_len, encapsulated_len_wrapped, encode, encoded_len, Encoder}, error::{Error, Result}, }; pub use base64ct::LineEnding; @@ -83,16 +83,18 @@ const POST_ENCAPSULATION_BOUNDARY: &[u8] = b"-----END "; /// Delimiter of encapsulation boundaries. const ENCAPSULATION_BOUNDARY_DELIMITER: &[u8] = b"-----"; -/// Width at which Base64 must be wrapped. +/// Width at which the Base64 body of RFC7468-compliant PEM is wrapped. /// -/// From RFC 7468 Section 2: +/// From [RFC7468 § 2]: /// /// > Generators MUST wrap the base64-encoded lines so that each line /// > consists of exactly 64 characters except for the final line, which /// > will encode the remainder of the data (within the 64-character line /// > boundary), and they MUST NOT emit extraneous whitespace. Parsers MAY /// > handle other line sizes. -const BASE64_WRAP_WIDTH: usize = 64; +/// +/// [RFC7468 § 2]: https://datatracker.ietf.org/doc/html/rfc7468#section-2 +pub const BASE64_WRAP_WIDTH: usize = 64; /// Buffered Base64 decoder type. pub type Base64Decoder<'i> = base64ct::Decoder<'i, base64ct::Base64>; diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index dea89f65e..0dc22c494 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -18,7 +18,7 @@ rust-version = "1.57" [dependencies] base64ct = { version = "1.4", path = "../base64ct" } -pem-rfc7468 = { version = "0.5", path = "../pem-rfc7468" } +pem-rfc7468 = { version = "=0.6.0-pre", path = "../pem-rfc7468" } zeroize = { version = "1", default-features = false } # optional dependencies diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index d0f2bf9d4..98b07e5f6 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -145,7 +145,6 @@ use core::str; #[cfg(feature = "alloc")] use { - crate::writer::base64_len, alloc::{string::String, vec::Vec}, zeroize::Zeroizing, }; @@ -262,7 +261,13 @@ impl PrivateKey { #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] pub fn to_openssh(&self, line_ending: LineEnding) -> Result> { - let encoded_len = self.pem_encoded_len(line_ending)?; + let encoded_len = pem::encapsulated_len_wrapped( + Self::PEM_LABEL, + PEM_LINE_WIDTH, + line_ending, + self.encoded_len()?, + )?; + let mut buf = vec![0u8; encoded_len]; let actual_len = self.encode_openssh(line_ending, &mut buf)?.len(); buf.truncate(actual_len); @@ -600,28 +605,6 @@ impl PrivateKey { ] .checked_sum() } - - /// Estimated length of a PEM-encoded key in OpenSSH format. - /// - /// May be slightly longer than the actual result. - #[cfg(feature = "alloc")] - fn pem_encoded_len(&self, line_ending: LineEnding) -> Result { - let base64_len = base64_len(self.encoded_len()?); - - // Add the length of the line endings which will be inserted when - // encoded Base64 is line wrapped - let newline_len = base64_len - .saturating_sub(1) - .checked_div(PEM_LINE_WIDTH) - .and_then(|len| len.checked_add(line_ending.len())) - .ok_or(Error::Length)?; - - Ok(pem::encapsulated_len( - Self::PEM_LABEL, - line_ending, - [base64_len, newline_len].checked_sum()?, - )?) - } } impl Decode for PrivateKey {