Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion der/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
15 changes: 1 addition & 14 deletions der/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,22 +98,9 @@ pub trait EncodePem: Encode + PemLabel {
#[cfg(feature = "pem")]
#[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
impl<T: Encode + PemLabel> EncodePem for T {
#[allow(clippy::integer_arithmetic)]
fn to_pem(&self, line_ending: LineEnding) -> Result<String> {
// 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)?;
Expand Down
2 changes: 1 addition & 1 deletion pem-rfc7468/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
135 changes: 105 additions & 30 deletions pem-rfc7468/src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize> {
[
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<usize> {
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<usize> {
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<usize> {
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.
Expand Down Expand Up @@ -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<usize> {
[
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<usize> {
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
Expand All @@ -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,
Expand Down Expand Up @@ -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())
}
}

Expand Down
10 changes: 6 additions & 4 deletions pem-rfc7468/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>;
Expand Down
2 changes: 1 addition & 1 deletion ssh-key/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 7 additions & 24 deletions ssh-key/src/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ use core::str;

#[cfg(feature = "alloc")]
use {
crate::writer::base64_len,
alloc::{string::String, vec::Vec},
zeroize::Zeroizing,
};
Expand Down Expand Up @@ -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<Zeroizing<String>> {
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);
Expand Down Expand Up @@ -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<usize> {
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 {
Expand Down