diff --git a/Cargo.toml b/Cargo.toml index d8abef3..e2911f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,6 @@ cast_possible_wrap = "warn" cast_precision_loss = "warn" cast_sign_loss = "warn" checked_conversions = "warn" -from_iter_instead_of_collect = "warn" implicit_saturating_sub = "warn" integer_division_remainder_used = "warn" manual_assert = "warn" diff --git a/ssh-encoding/src/decoder.rs b/ssh-encoding/src/decoder.rs index 031d242..ec9e349 100644 --- a/ssh-encoding/src/decoder.rs +++ b/ssh-encoding/src/decoder.rs @@ -15,9 +15,9 @@ use core::{fmt, str}; /// /// # Example /// ``` -/// use ssh_encoding::{Decode, Decoder}; +/// use ssh_encoding::{Decode, Reader}; /// -/// let mut decoder: Decoder<_> = [0, 0, 0, 42].as_slice().into(); +/// let mut decoder = [0, 0, 0, 42].as_slice().into_decoder(); /// let value = u32::decode(&mut decoder)?; /// assert_eq!(decoder.finish(value)?, 42); /// # Ok::<(), ssh_encoding::Error>(()) diff --git a/ssh-encoding/src/lib.rs b/ssh-encoding/src/lib.rs index e85eeb2..a3f70b0 100644 --- a/ssh-encoding/src/lib.rs +++ b/ssh-encoding/src/lib.rs @@ -72,12 +72,13 @@ //! //! Stored as a byte string, 8 bits per byte, MSB first (a.k.a. big endian). //! -//! Negative numbers have the value 1 as the most significant bit of the first byte of -//! the data partition. If the most significant bit would be set for -//! a positive number, the number MUST be preceded by a zero byte. -//! Unnecessary leading bytes with the value 0 or 255 MUST NOT be -//! included. The value zero MUST be stored as a string with zero -//! bytes of data. +//! This crate supports only non-negative values, matching OpenSSH, and enforces a 16384-bit +//! magnitude limit. Negative wire encodings are rejected. A leading zero byte is added when +//! the most significant bit is set. Zero is encoded as an empty string. +//! +//! Decoding accepts redundant leading zeros like OpenSSH, subject to a 2049-byte wire limit +//! (the extra byte must be zero). Encoding always produces the canonical representation. +//! The byte APIs use unsigned magnitudes without sign padding or the length prefix. //! //! By convention, a number that is used in modular computations in //! `Z_n` SHOULD be represented in the range `0 <= x < n`. @@ -89,8 +90,6 @@ //! `0` | `00 00 00 00` //! `9a378f9b2e332a7` | `00 00 00 08 09 a3 78 f9 b2 e3 32 a7` //! `80` | `00 00 00 02 00 80` -//! `-1234` | `00 00 00 02 ed cc` -//! `-deadbeef` | `00 00 00 05 ff 21 52 41 11` //! //! ### `name-list`: string containing a comma-separated list of names //! #### [`Decode`]/[`Encode`] trait impls: `Vec` diff --git a/ssh-encoding/src/mpint.rs b/ssh-encoding/src/mpint.rs index 8e5fe63..a14d23e 100644 --- a/ssh-encoding/src/mpint.rs +++ b/ssh-encoding/src/mpint.rs @@ -1,148 +1,96 @@ //! Multiple precision integer. use crate::{CheckedSum, Decode, Decoder, Encode, Error, Reader, Result, Writer}; -use alloc::{boxed::Box, vec::Vec}; +use alloc::boxed::Box; use core::fmt; #[cfg(feature = "bigint")] use crate::Uint; - -#[cfg(feature = "ctutils")] -use ctutils::{Choice, CtEq}; - -#[cfg(any(feature = "bigint", feature = "zeroize"))] +#[cfg(feature = "zeroize")] use zeroize::Zeroize; #[cfg(feature = "bigint")] use zeroize::Zeroizing; +#[cfg(feature = "ctutils")] +use { + core::cmp::Ordering, + ctutils::{Choice, CtEq, CtLt, CtSelect}, +}; /// Multiple precision integer, a.k.a. `mpint`. /// -/// Described in [RFC4251 § 5](https://datatracker.ietf.org/doc/html/rfc4251#section-5): +/// This type supports the non-negative subset of [RFC4251 § 5], matching [OpenSSH]. +/// Values are limited to 16384-bits. Negative SSH encodings are rejected. /// -/// > Represents multiple precision integers in two's complement format, -/// > stored as a string, 8 bits per byte, MSB first. Negative numbers -/// > have the value 1 as the most significant bit of the first byte of -/// > the data partition. If the most significant bit would be set for -/// > a positive number, the number MUST be preceded by a zero byte. -/// > Unnecessary leading bytes with the value 0 or 255 MUST NOT be -/// > included. The value zero MUST be stored as a string with zero -/// > bytes of data. -/// > -/// > By convention, a number that is used in modular computations in -/// > Z_n SHOULD be represented in the range 0 <= x < n. +/// [`Self::from_bytes`] and [`Self::as_bytes`] use an unsigned big endian magnitude, +/// without a length prefix or sign padding. Leading zeros are stripped and zero +/// is represented by an empty slice. +/// +/// [`Decode`] accepts redundant leading zeros like OpenSSH, but limits the input +/// to [`Self::MAX_SIZE`] bytes plus one leading zero byte *before* stripping zeros. +/// [`Encode`] always emits the canonical SSH encoding, including a leading zero +/// when the most significant bit is set and an empty string for zero. +/// +/// With the `ctutils` feature, constant-time order and equality operations on equal-length +/// magnitudes are supported. Lengths are not treated as secret. Byte conversion, decoding, and +/// encoding are variable-time. /// /// ## Examples /// -/// | value (hex) | representation (hex) | -/// |-----------------|----------------------| -/// | 0 | `00 00 00 00` -/// | 9a378f9b2e332a7 | `00 00 00 08 09 a3 78 f9 b2 e3 32 a7` -/// | 80 | `00 00 00 02 00 80` -/// |-1234 | `00 00 00 02 ed cc` -/// | -deadbeef | `00 00 00 05 ff 21 52 41 11` -#[cfg_attr(not(feature = "ctutils"), derive(Clone))] -#[cfg_attr(feature = "ctutils", derive(Clone, Ord, PartialOrd))] // TODO: constant time (Partial)`Ord`? +/// | value (hex) | SSH encoding (hex) | +/// |-----------------|--------------------| +/// | 0 | `00 00 00 00` | +/// | 9a378f9b2e332a7 | `00 00 00 08 09 a3 78 f9 b2 e3 32 a7` | +/// | 80 | `00 00 00 02 00 80` | +/// +/// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 +/// [OpenSSH]: https://github.com/openssh/openssh-portable/blob/1d28115e040b792075a391de01b0f0c033a4850a/sshbuf-getput-basic.c#L598 +#[derive(Clone)] +#[cfg_attr(not(feature = "ctutils"), derive(Eq, PartialEq))] pub struct Mpint { - /// Inner big endian-serialized integer value + /// Unsigned big endian magnitude without leading zeros. Empty for zero. inner: Box<[u8]>, } impl Mpint { - /// Maximum size in bytes accepted by fallible constructors and bigint conversions, - /// excluding an optional leading sign byte. Matches OpenSSH's 16384-bit limit. + /// Maximum magnitude size in bytes, excluding SSH sign padding. + /// + /// This 16384-bit limit is enforced by every constructor and decoder. pub const MAX_SIZE: usize = 2048; - /// Create a new multiple precision integer from the given big endian-encoded byte slice. + /// Create an integer from its unsigned big endian magnitude. /// - /// Note that this method expects a leading zero on positive integers whose MSB is set, but does - /// *NOT* expect a 4-byte length prefix. + /// Leading zeros are stripped. The most significant bit is part of the magnitude, + /// not a sign bit. This method does not expect an SSH length prefix or sign padding. /// /// # Errors - /// Returns [`Error::MpintEncoding`] in the event of an unnecessary leading `0`. - /// Returns [`Error::Length`] if the integer exceeds [`Self::MAX_SIZE`]. + /// Returns [`Error::Length`] if the magnitude exceeds [`Self::MAX_SIZE`]. pub fn from_bytes(bytes: &[u8]) -> Result { - bytes.try_into() - } - - /// Create a new multiple precision integer from the given big endian encoded byte slice - /// representing a positive integer. - /// - /// The input may begin with leading zeros, which will be stripped when converted to [`Mpint`] - /// encoding. - /// - /// This infallible constructor does not enforce [`Self::MAX_SIZE`]. Conversion to a bigint - /// still rejects oversized values. - // TODO(tarcieri): make this fallible and enforce `MAX_SIZE` in the next breaking release. - #[must_use] - pub fn from_positive_bytes(mut bytes: &[u8]) -> Self { - // Strip leading zeros - while bytes.first().copied() == Some(0) { - bytes = &bytes[1..]; - } - - // Add a leading zero to the output if necessary - let inner = match bytes.first().copied() { - Some(n) if n >= 0x80 => { - let mut inner = Vec::with_capacity(bytes.len().saturating_add(1)); - inner.push(0); - inner.extend_from_slice(bytes); - inner - } - _ => Vec::from(bytes), - }; - - Self { - inner: inner.into_boxed_slice(), + let bytes = strip_leading_zeros(bytes); + if bytes.len() > Self::MAX_SIZE { + return Err(Error::Length); } + Ok(Self { + inner: bytes.into(), + }) } - /// Get the big integer data encoded as big endian bytes. + /// Get the unsigned big endian magnitude, without leading zeros or SSH sign padding. /// - /// This slice will contain a leading zero if the value is positive but the - /// MSB is also set. Use [`Mpint::as_positive_bytes`] to ensure the number - /// is positive and strip the leading zero byte if it exists. + /// Returns an empty slice for zero. #[must_use] pub fn as_bytes(&self) -> &[u8] { &self.inner } - /// Get the bytes of a positive integer. - /// - /// # Returns - /// - `Some(bytes)` if the number is positive. The leading zero byte will be stripped. - /// - `None` if the value is negative + /// Is this integer zero? #[must_use] - pub fn as_positive_bytes(&self) -> Option<&[u8]> { - match self.as_bytes() { - [0x00, rest @ ..] => Some(rest), - [byte, ..] if *byte < 0x80 => Some(self.as_bytes()), - _ => None, - } + pub fn is_zero(&self) -> bool { + self.inner.is_empty() } - /// Is this [`Mpint`] positive? - #[must_use] - pub fn is_positive(&self) -> bool { - self.as_positive_bytes().is_some() - } - - /// Validate the size and encoding before allocating or accepting an owned buffer. - fn validate(bytes: &[u8]) -> Result<()> { - let value = match bytes { - [0x00 | 0xff, rest @ ..] => rest, - _ => bytes, - }; - - if value.len() > Self::MAX_SIZE { - return Err(Error::Length); - } - - match bytes { - // Unnecessary leading 0 - [0x00] => Err(Error::MpintEncoding), - [0x00, n, ..] if *n < 0x80 => Err(Error::MpintEncoding), - _ => Ok(()), - } + /// Does the SSH encoding require a leading zero to avoid a negative sign bit? + fn needs_padding(&self) -> bool { + self.inner.first().is_some_and(|byte| *byte >= 0x80) } } @@ -155,7 +103,7 @@ impl AsRef<[u8]> for Mpint { #[cfg(feature = "ctutils")] impl CtEq for Mpint { fn ct_eq(&self, other: &Self) -> Choice { - self.as_ref().ct_eq(other.as_ref()) + self.as_bytes().ct_eq(other.as_bytes()) } } @@ -169,31 +117,86 @@ impl PartialEq for Mpint { } } +#[cfg(feature = "ctutils")] +impl Ord for Mpint { + fn cmp(&self, other: &Self) -> Ordering { + // Canonical magnitudes with different byte lengths can be ordered by length. + match self.inner.len().cmp(&other.inner.len()) { + Ordering::Equal => {} + ordering => return ordering, + } + + // Visit every byte, with more significant differences taking precedence. + let mut ordering = Ordering::Equal; + for (a, b) in self.inner.iter().zip(other.inner.iter()).rev() { + ordering = ordering.ct_select(&Ordering::Less, a.ct_lt(b)); + ordering = ordering.ct_select(&Ordering::Greater, b.ct_lt(a)); + } + ordering + } +} + +#[cfg(feature = "ctutils")] +impl PartialOrd for Mpint { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl Decode for Mpint { type Error = Error; fn decode(reader: &mut Decoder) -> Result { reader.read_prefixed(|reader| { let len = reader.remaining_len(); + if len > Self::MAX_SIZE + 1 { return Err(Error::Length); + } else if len == 0 { + return Self::from_bytes(&[]); + } + + let mut first = u8::decode(reader)?; + + if first >= 0x80 { + return Err(Error::MpintEncoding); + } else if first != 0 && len > Self::MAX_SIZE { + return Err(Error::Length); } - let mut bytes = vec![0; len]; - reader.read(&mut bytes)?; - bytes.into_boxed_slice().try_into() + // Like OpenSSH, accept redundant leading zeros, including noncanonical zero. + while first == 0 { + if reader.is_finished() { + return Self::from_bytes(&[]); + } + first = u8::decode(reader)?; + } + + let mut bytes = vec![0; reader.remaining_len() + 1]; + bytes[0] = first; + reader.read(&mut bytes[1..])?; + + Ok(Self { + inner: bytes.into_boxed_slice(), + }) }) } } impl Encode for Mpint { fn encoded_len(&self) -> Result { - [4, self.as_bytes().len()].checked_sum() + [4, self.inner.len(), usize::from(self.needs_padding())].checked_sum() } fn encode(&self, writer: &mut impl Writer) -> Result<()> { - self.as_bytes().encode(writer)?; - Ok(()) + let padding = self.needs_padding(); + (self.inner.len() + usize::from(padding)).encode(writer)?; + + if padding { + 0u8.encode(writer)?; + } + + writer.write(self.as_bytes()) } } @@ -201,10 +204,7 @@ impl TryFrom<&[u8]> for Mpint { type Error = Error; fn try_from(bytes: &[u8]) -> Result { - Self::validate(bytes)?; - Ok(Self { - inner: bytes.into(), - }) + Self::from_bytes(bytes) } } @@ -212,8 +212,19 @@ impl TryFrom> for Mpint { type Error = Error; fn try_from(bytes: Box<[u8]>) -> Result { - Self::validate(&bytes)?; - Ok(Self { inner: bytes }) + if bytes.first() == Some(&0) { + let result = Self::from_bytes(&bytes); + #[cfg(feature = "zeroize")] + { + let mut bytes = bytes; + bytes.zeroize(); + } + result + } else if bytes.len() > Self::MAX_SIZE { + Err(Error::Length) + } else { + Ok(Self { inner: bytes }) + } } } @@ -221,6 +232,8 @@ impl TryFrom> for Mpint { impl Zeroize for Mpint { fn zeroize(&mut self) { self.inner.zeroize(); + // Preserve the canonical representation of zero after clearing the buffer. + self.inner = Box::default(); } } @@ -238,95 +251,215 @@ impl fmt::Display for Mpint { impl fmt::LowerHex for Mpint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_zero() { + return f.write_str("0"); + } + for byte in self.as_bytes() { write!(f, "{byte:02x}")?; } + Ok(()) } } impl fmt::UpperHex for Mpint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_zero() { + return f.write_str("0"); + } + for byte in self.as_bytes() { write!(f, "{byte:02X}")?; } + Ok(()) } } -// TODO(tarcieri): replace with TryFrom and enforce `MAX_SIZE` in the next breaking release. #[cfg(feature = "bigint")] -impl From<&Uint> for Mpint { - fn from(uint: &Uint) -> Mpint { +impl TryFrom<&Uint> for Mpint { + type Error = Error; + + fn try_from(uint: &Uint) -> Result { + // Reject oversized values before allocating their byte representation. + // TODO(tarcieri): use `bit_width_vartime` when we bump `crypto-bigint` version + #[allow(deprecated, reason = "crypto-bigint v0.7.0 compatibility")] + let bits = uint.bits_vartime(); + + if bits > u32::try_from(Self::MAX_SIZE * 8)? { + return Err(Error::Length); + } let bytes = Zeroizing::new(uint.to_be_bytes()); - Mpint::from_positive_bytes(&bytes) + Self::from_bytes(&bytes) } } -// TODO(tarcieri): replace with TryFrom and enforce `MAX_SIZE` in the next breaking release. #[cfg(feature = "bigint")] -impl From for Mpint { - fn from(uint: Uint) -> Mpint { - Mpint::from(&uint) +impl TryFrom for Mpint { + type Error = Error; + + fn try_from(uint: Uint) -> Result { + Self::try_from(&uint) } } #[cfg(feature = "bigint")] -impl TryFrom for Uint { - type Error = Error; - - fn try_from(mpint: Mpint) -> Result { - Uint::try_from(&mpint) +impl From for Uint { + fn from(mpint: Mpint) -> Uint { + Uint::from(&mpint) } } #[cfg(feature = "bigint")] -impl TryFrom<&Mpint> for Uint { - type Error = Error; - - fn try_from(mpint: &Mpint) -> Result { - let bytes = mpint.as_positive_bytes().ok_or(Error::MpintEncoding)?; - if bytes.len() > Mpint::MAX_SIZE { - return Err(Error::Length); - } +impl From<&Mpint> for Uint { + fn from(mpint: &Mpint) -> Uint { + Uint::from_be_slice_vartime(mpint.as_bytes()) + } +} - Ok(Uint::from_be_slice_vartime(bytes)) +/// Strip leading zeros from an unsigned big endian magnitude. +fn strip_leading_zeros(mut bytes: &[u8]) -> &[u8] { + while bytes.first() == Some(&0) { + bytes = &bytes[1..]; } + bytes } #[cfg(test)] mod tests { use super::Mpint; use crate::{Decode, Encode, Error, Reader}; - use alloc::vec::Vec; + use alloc::{format, vec::Vec}; use hex_literal::hex; + #[test] + fn unsigned_bytes() { + for (bytes, expected) in [ + (&b""[..], &b""[..]), + (&hex!("00")[..], &b""[..]), + (&hex!("00 00")[..], &b""[..]), + (&hex!("00 01")[..], &hex!("01")[..]), + (&hex!("00 00 80")[..], &hex!("80")[..]), + (&hex!("80")[..], &hex!("80")[..]), + (&hex!("ff")[..], &hex!("ff")[..]), + ] { + for n in [ + Mpint::from_bytes(bytes).unwrap(), + Mpint::try_from(bytes).unwrap(), + Mpint::try_from(Vec::from(bytes).into_boxed_slice()).unwrap(), + ] { + assert_eq!(n.as_bytes(), expected); + assert_eq!(n.is_zero(), expected.is_empty()); + } + } + } + + #[test] + fn ssh_encodings() { + for (bytes, encoded) in [ + (&b""[..], &hex!("00000000")[..]), + (&hex!("01")[..], &hex!("00000001 01")[..]), + (&hex!("7f")[..], &hex!("00000001 7f")[..]), + (&hex!("80")[..], &hex!("00000002 0080")[..]), + (&hex!("ff")[..], &hex!("00000002 00ff")[..]), + ( + &hex!("09 a3 78 f9 b2 e3 32 a7")[..], + &hex!("00000008 09 a3 78 f9 b2 e3 32 a7")[..], + ), + ] { + let n = Mpint::from_bytes(bytes).unwrap(); + let mut output = Vec::new(); + n.encode(&mut output).unwrap(); + assert_eq!(output, encoded); + assert_eq!(n.encoded_len().unwrap(), output.len()); + assert_eq!(Mpint::decode(&mut encoded.into()).unwrap(), n); + } + } + + #[test] + fn normalize_ssh_padding() { + for bytes in [ + &hex!("00")[..], + &hex!("00 00")[..], + &hex!("00 01")[..], + &hex!("00 00 80")[..], + ] { + let mut encoded = Vec::new(); + bytes.encode(&mut encoded).unwrap(); + let n = Mpint::decode(&mut encoded.as_slice().into()).unwrap(); + assert_eq!(n, Mpint::from_bytes(bytes).unwrap()); + let mut canonical = Vec::new(); + n.encode(&mut canonical).unwrap(); + assert!(canonical.len() < encoded.len()); + } + } + + #[test] + fn reject_negative() { + for bytes in [ + &hex!("80")[..], + &hex!("ff")[..], + &hex!("ed cc")[..], + &hex!("ff 21 52 41 11")[..], + ] { + let mut encoded = Vec::new(); + bytes.encode(&mut encoded).unwrap(); + let mut reader = encoded.as_slice().into_decoder(); + assert_eq!(Mpint::decode(&mut reader), Err(Error::MpintEncoding)); + assert_eq!(reader.finish(()), Err(Error::Failed)); + } + } + #[test] fn size_limits() { - // Include positive and negative values, with and without a leading sign byte. + for size in [Mpint::MAX_SIZE, Mpint::MAX_SIZE + 1] { + for first in [0x01, 0x7f, 0x80, 0xff] { + let mut bytes = vec![0xff; size]; + bytes[0] = first; + // Leading zeros in an unsigned input do not change the magnitude. + for padding in [0, 1, 2] { + let mut padded = vec![0; padding]; + padded.extend_from_slice(&bytes); + for result in [ + Mpint::from_bytes(&padded), + Mpint::try_from(padded.clone().into_boxed_slice()), + ] { + if size == Mpint::MAX_SIZE { + let n = result.unwrap(); + assert_eq!(n.as_bytes(), bytes); + let mut encoded = Vec::new(); + n.encode(&mut encoded).unwrap(); + assert_eq!(n.encoded_len().unwrap(), encoded.len()); + assert_eq!(Mpint::decode(&mut encoded.as_slice().into()).unwrap(), n); + } else { + assert_eq!(result, Err(Error::Length)); + } + } + } + } + } + } + + #[test] + fn ssh_size_limits_before_normalization() { for (first, size, valid) in [ (0x7f, Mpint::MAX_SIZE, true), - (0x80, Mpint::MAX_SIZE, true), (0x00, Mpint::MAX_SIZE + 1, true), - (0xff, Mpint::MAX_SIZE + 1, true), (0x01, Mpint::MAX_SIZE + 1, false), - (0x80, Mpint::MAX_SIZE + 1, false), (0x00, Mpint::MAX_SIZE + 2, false), - (0xff, Mpint::MAX_SIZE + 2, false), ] { - let mut bytes = vec![if first == 0xff { 0x7f } else { 0x80 }; size]; - bytes[0] = first; - let borrowed = Mpint::from_bytes(&bytes); - let owned = Mpint::try_from(bytes.clone().into_boxed_slice()); - let mut encoded = Vec::new(); - bytes.encode(&mut encoded).unwrap(); - let decoded = Mpint::decode(&mut encoded.as_slice().into()); - - for result in [borrowed, owned, decoded] { + // Even all-zero encodings must obey the wire size limit. + for rest in [0, 0xff] { + let mut bytes = vec![rest; size]; + bytes[0] = first; + let mut encoded = Vec::new(); + bytes.encode(&mut encoded).unwrap(); + let result = Mpint::decode(&mut encoded.as_slice().into()); if valid { - assert_eq!(result.unwrap().as_bytes(), bytes); + assert_eq!(result.unwrap(), Mpint::from_bytes(&bytes).unwrap()); } else { - assert!(matches!(result, Err(Error::Length))); + assert_eq!(result, Err(Error::Length)); } } } @@ -334,7 +467,6 @@ mod tests { #[test] fn reject_oversized_before_reading_body() { - // A reader whose body cannot be read demonstrates rejection at the length check. struct Unreadable { prefix: Option<[u8; 4]>, } @@ -357,82 +489,80 @@ mod tests { let reader = Unreadable { prefix: Some(u32::try_from(Mpint::MAX_SIZE + 2).unwrap().to_be_bytes()), }; - assert!(matches!( - Mpint::decode(&mut reader.into()), - Err(Error::Length) - )); + assert_eq!(Mpint::decode(&mut reader.into()), Err(Error::Length)); } - #[cfg(feature = "bigint")] #[test] - fn bigint_size_limits() { - use crate::Uint; - - for size in [Mpint::MAX_SIZE, Mpint::MAX_SIZE + 1] { - let bytes = vec![0x80; size]; - let uint = Uint::from_be_slice_vartime(&bytes); - // All infallible constructors remain available, but cannot bypass the bigint limit. - for mpint in [ - Mpint::from_positive_bytes(&bytes), - Mpint::from(&uint), - Mpint::from(uint.clone()), - ] { - let borrowed = Uint::try_from(&mpint); - let owned = Uint::try_from(mpint); - for result in [borrowed, owned] { - if size == Mpint::MAX_SIZE { - assert_eq!(result.unwrap(), uint); - } else { - assert!(matches!(result, Err(Error::Length))); - } - } + #[cfg(feature = "ctutils")] + fn numeric_ordering() { + let values = [ + 0u32, 1, 0x7f, 0x80, 0xff, 0x100, 0x101, 0x1ff, 0x200, 0xffff, 0x10000, + ]; + for a in values { + for b in values { + let x = Mpint::from_bytes(&a.to_be_bytes()).unwrap(); + let y = Mpint::from_bytes(&b.to_be_bytes()).unwrap(); + assert_eq!(x == y, a == b); + assert_eq!(x.cmp(&y), a.cmp(&b)); + assert_eq!(x.partial_cmp(&y), a.partial_cmp(&b)); } } } #[test] - fn decode_0() { - let n = Mpint::from_bytes(b"").unwrap(); - assert_eq!(b"", n.as_bytes()); - } - - #[test] - fn reject_extra_leading_zeroes() { - assert!(Mpint::from_bytes(&hex!("00")).is_err()); - assert!(Mpint::from_bytes(&hex!("00 00")).is_err()); - assert!(Mpint::from_bytes(&hex!("00 01")).is_err()); - } - - #[test] - fn decode_9a378f9b2e332a7() { - assert!(Mpint::from_bytes(&hex!("09 a3 78 f9 b2 e3 32 a7")).is_ok()); - } - + fn formatting() { + let zero = Mpint::from_bytes(&[]).unwrap(); + assert_eq!( + format!("{zero} {zero:x} {zero:X} {zero:?}"), + "0 0 0 Mpint(0)" + ); + let n = Mpint::from_bytes(&hex!("00 ab cd")).unwrap(); + assert_eq!( + format!("{n} {n:x} {n:X} {n:?}"), + "ABCD abcd ABCD Mpint(ABCD)" + ); + } + + #[cfg(feature = "zeroize")] #[test] - fn decode_80() { - let n = Mpint::from_bytes(&hex!("00 80")).unwrap(); - - // Leading zero stripped - assert_eq!(&hex!("80"), n.as_positive_bytes().unwrap()); - } - #[test] - fn from_positive_bytes_strips_leading_zeroes() { - assert_eq!(Mpint::from_positive_bytes(&hex!("00")).as_ref(), b""); - assert_eq!(Mpint::from_positive_bytes(&hex!("00 00")).as_ref(), b""); - assert_eq!(Mpint::from_positive_bytes(&hex!("00 01")).as_ref(), b"\x01"); + fn zeroize_preserves_invariants() { + use zeroize::Zeroize; + let mut n = Mpint::from_bytes(&hex!("80 ff")).unwrap(); + n.zeroize(); + assert!(n.is_zero()); + assert_eq!(n, Mpint::from_bytes(&[]).unwrap()); + let mut encoded = Vec::new(); + n.encode(&mut encoded).unwrap(); + assert_eq!(encoded, hex!("00000000")); } - // TODO(tarcieri): drop support for negative numbers? - #[test] - fn decode_neg_1234() { - let n = Mpint::from_bytes(&hex!("ed cc")).unwrap(); - assert!(n.as_positive_bytes().is_none()); - } - - // TODO(tarcieri): drop support for negative numbers? + #[cfg(feature = "bigint")] #[test] - fn decode_neg_deadbeef() { - let n = Mpint::from_bytes(&hex!("ff 21 52 41 11")).unwrap(); - assert!(n.as_positive_bytes().is_none()); + fn bigint_conversions() { + use crate::Uint; + for bytes in [ + vec![], + vec![0], + vec![0x80], + vec![0xff; Mpint::MAX_SIZE], + vec![0xff; Mpint::MAX_SIZE + 1], + ] { + let uint = Uint::from_be_slice_vartime(&bytes); + for result in [Mpint::try_from(&uint), Mpint::try_from(uint.clone())] { + if bytes.len() <= Mpint::MAX_SIZE { + let n = result.unwrap(); + assert_eq!(n, Mpint::from_bytes(&bytes).unwrap()); + assert_eq!(Uint::from(&n), uint); + assert_eq!(Uint::from(n), uint); + } else { + assert_eq!(result, Err(Error::Length)); + } + } + } + // Precision larger than the cap is allowed when the magnitude fits. + let mut padded = vec![0; Mpint::MAX_SIZE + 1]; + padded[Mpint::MAX_SIZE] = 1; + let uint = Uint::from_be_slice_vartime(&padded); + assert_eq!(Mpint::try_from(&uint).unwrap().as_bytes(), &[1]); } } diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index 835bc30..c71b369 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -485,22 +485,11 @@ fn decode_private_key_as( #[cfg(feature = "ed25519")] (Algorithm::Ed25519, KeyData::Ed25519(pk)) => { - // PPK encodes Ed25519 private exponent as an mpint - use crate::Mpint; use crate::private::{Ed25519Keypair, Ed25519PrivateKey}; use zeroize::Zeroizing; - // Copy and pad exponent let mut buf = Zeroizing::new([0u8; Ed25519PrivateKey::BYTE_SIZE]); - let e = Mpint::decode(reader)?; - let e_bytes = e.as_bytes(); - - if e_bytes.len() > buf.len() { - return Err(Error::Crypto); - } - - #[allow(clippy::arithmetic_side_effects)] // length checked - buf[Ed25519PrivateKey::BYTE_SIZE - e_bytes.len()..].copy_from_slice(e_bytes); + reader.read_prefixed(|reader| reader.read(buf.as_mut()))?; let private = Ed25519PrivateKey::from_bytes(&buf); Ok(KeypairData::Ed25519(Ed25519Keypair { @@ -518,7 +507,7 @@ fn decode_private_key_as( // Copy and pad exponent let e = Mpint::decode(reader)?; - let e_bytes = e.as_positive_bytes().ok_or(Error::Crypto)?; + let e_bytes = e.as_bytes(); if e_bytes.len() > curve.field_size() { return Err(Error::Crypto); } @@ -556,3 +545,29 @@ fn decode_private_key_as( _ => Err(algorithm.unsupported_error()), } } + +#[cfg(all(test, feature = "ed25519"))] +mod tests { + use super::*; + use crate::private::Ed25519PrivateKey; + + #[test] + fn ed25519_seed_is_not_an_mpint() { + // Cover both a set sign bit and leading zeros that mpint decoding would strip. + for first in [0, 0x7f, 0x80, 0xff] { + let mut bytes = [0x42; Ed25519PrivateKey::BYTE_SIZE]; + bytes[0] = first; + let private = Ed25519PrivateKey::from_bytes(&bytes); + let public = crate::public::Ed25519PublicKey::from(&private); + let mut encoded = Vec::new(); + bytes.as_slice().encode(&mut encoded).unwrap(); + let key = decode_private_key_as( + &mut encoded.as_slice().into(), + PublicKey::new(public.into(), ""), + Algorithm::Ed25519, + ) + .unwrap(); + assert_eq!(key.ed25519().unwrap().private.as_ref(), &bytes); + } + } +} diff --git a/ssh-key/src/private/dsa.rs b/ssh-key/src/private/dsa.rs index 7a440fe..172625f 100644 --- a/ssh-key/src/private/dsa.rs +++ b/ssh-key/src/private/dsa.rs @@ -27,13 +27,12 @@ impl DsaPrivateKey { /// Create a new DSA private key given the value `x`. /// /// # Errors - /// Returns [`Error::FormatEncoding`] if `x` is negative. + /// Returns [`Error::FormatEncoding`] if `x` is zero. pub fn new(x: Mpint) -> Result { - if x.is_positive() { - Ok(Self { inner: x }) - } else { - Err(Error::FormatEncoding) + if x.is_zero() { + return Err(Error::FormatEncoding); } + Ok(Self { inner: x }) } /// Get the serialized private key as bytes. @@ -108,36 +107,34 @@ impl Drop for DsaPrivateKey { } #[cfg(feature = "dsa")] -impl From<&dsa::SigningKey> for DsaPrivateKey { - fn from(key: &dsa::SigningKey) -> DsaPrivateKey { - DsaPrivateKey { - inner: key.x().as_ref().into(), - } - } -} +impl TryFrom<&dsa::SigningKey> for DsaPrivateKey { + type Error = Error; -#[cfg(feature = "dsa")] -impl From for DsaPrivateKey { - fn from(key: dsa::SigningKey) -> DsaPrivateKey { - DsaPrivateKey::from(&key) + fn try_from(key: &dsa::SigningKey) -> Result { + Self::new(key.x().as_ref().try_into()?) } } #[cfg(feature = "dsa")] -impl TryFrom for Uint { +impl TryFrom for DsaPrivateKey { type Error = Error; - fn try_from(key: DsaPrivateKey) -> Result { - Ok(Uint::try_from(&key.inner)?) + fn try_from(key: dsa::SigningKey) -> Result { + DsaPrivateKey::try_from(&key) } } #[cfg(feature = "dsa")] -impl TryFrom<&DsaPrivateKey> for Uint { - type Error = Error; +impl From for Uint { + fn from(key: DsaPrivateKey) -> Uint { + Uint::from(&key.inner) + } +} - fn try_from(key: &DsaPrivateKey) -> Result { - Ok(Uint::try_from(&key.inner)?) +#[cfg(feature = "dsa")] +impl From<&DsaPrivateKey> for Uint { + fn from(key: &DsaPrivateKey) -> Uint { + Uint::from(&key.inner) } } @@ -164,7 +161,7 @@ impl DsaKeypair { let Ok(components) = dsa::Components::try_generate_from_rng_with_key_size(rng, Self::KEY_SIZE); let Ok(sk) = dsa::SigningKey::try_generate_from_rng_with_components(rng, components); - Ok(sk.into()) + sk.try_into() } /// Create a new [`DsaKeypair`] with the given `public` and `private` components. @@ -260,24 +257,28 @@ impl TryFrom<&DsaKeypair> for dsa::SigningKey { fn try_from(key: &DsaKeypair) -> Result { Ok(dsa::SigningKey::from_components( dsa::VerifyingKey::try_from(&key.public)?, - key.private.as_mpint().try_into()?, + key.private.as_mpint().into(), )?) } } #[cfg(feature = "dsa")] -impl From for DsaKeypair { - fn from(key: dsa::SigningKey) -> DsaKeypair { - DsaKeypair::from(&key) +impl TryFrom for DsaKeypair { + type Error = Error; + + fn try_from(key: dsa::SigningKey) -> Result { + DsaKeypair::try_from(&key) } } #[cfg(feature = "dsa")] -impl From<&dsa::SigningKey> for DsaKeypair { - fn from(key: &dsa::SigningKey) -> DsaKeypair { - DsaKeypair { - private: key.into(), - public: key.verifying_key().into(), - } +impl TryFrom<&dsa::SigningKey> for DsaKeypair { + type Error = Error; + + fn try_from(key: &dsa::SigningKey) -> Result { + Ok(DsaKeypair { + private: key.try_into()?, + public: key.verifying_key().try_into()?, + }) } } diff --git a/ssh-key/src/private/rsa.rs b/ssh-key/src/private/rsa.rs index f87f0e2..ceaa86b 100644 --- a/ssh-key/src/private/rsa.rs +++ b/ssh-key/src/private/rsa.rs @@ -39,13 +39,12 @@ impl RsaPrivateKey { /// - `q`: Second prime factor of `n`. /// /// # Errors - /// Returns [`Error::FormatEncoding`] if any of the provided values are negative. + /// Returns [`Error::FormatEncoding`] if any of the provided values are zero. pub fn new(d: Mpint, iqmp: Mpint, p: Mpint, q: Mpint) -> Result { - if d.is_positive() && iqmp.is_positive() && p.is_positive() && q.is_positive() { - Ok(Self { d, iqmp, p, q }) - } else { - Err(Error::FormatEncoding) + if d.is_zero() || iqmp.is_zero() || p.is_zero() || q.is_zero() { + return Err(Error::FormatEncoding); } + Ok(Self { d, iqmp, p, q }) } /// RSA private exponent. @@ -259,13 +258,10 @@ impl TryFrom<&RsaKeypair> for rsa::PrivateKey { fn try_from(key: &RsaKeypair) -> Result { let ret = rsa::PrivateKey::from_components( - Uint::try_from(key.public.n())?, - Uint::try_from(key.public.e())?, - Uint::try_from(&key.private.d)?, - vec![ - Uint::try_from(&key.private.p)?, - Uint::try_from(&key.private.q)?, - ], + Uint::from(key.public.n()), + Uint::from(key.public.e()), + Uint::from(&key.private.d), + vec![Uint::from(&key.private.p), Uint::from(&key.private.q)], )?; Ok(ret) @@ -298,10 +294,10 @@ impl TryFrom<&rsa::PrivateKey> for RsaKeypair { let iqmp = key.crt_coefficient().ok_or(Error::Crypto)?; let private = RsaPrivateKey { - d: key.d().into(), - iqmp: iqmp.into(), - p: p.as_ref().into(), - q: q.as_ref().into(), + d: key.d().try_into()?, + iqmp: iqmp.try_into()?, + p: p.as_ref().try_into()?, + q: q.as_ref().try_into()?, }; Ok(RsaKeypair { public, private }) diff --git a/ssh-key/src/public/dsa.rs b/ssh-key/src/public/dsa.rs index 4a5d3d3..197a92a 100644 --- a/ssh-key/src/public/dsa.rs +++ b/ssh-key/src/public/dsa.rs @@ -36,13 +36,12 @@ impl DsaPublicKey { /// - `y`: the public key, where `y = gˣ mod p`. /// /// # Errors - /// Returns [`Error::FormatEncoding`] in the event any of the components are negative. + /// Returns [`Error::FormatEncoding`] if any component is zero. pub fn new(p: Mpint, q: Mpint, g: Mpint, y: Mpint) -> Result { - if p.is_positive() && q.is_positive() && g.is_positive() && y.is_positive() { - Ok(Self { p, q, g, y }) - } else { - Err(Error::FormatEncoding) + if p.is_zero() || q.is_zero() || g.is_zero() || y.is_zero() { + return Err(Error::FormatEncoding); } + Ok(Self { p, q, g, y }) } /// Prime modulus. @@ -126,10 +125,10 @@ impl TryFrom<&DsaPublicKey> for dsa::VerifyingKey { type Error = Error; fn try_from(key: &DsaPublicKey) -> Result { - let p = Uint::try_from(&key.p)?; - let q = Uint::try_from(&key.q)?; - let g = Uint::try_from(&key.g)?; - let y = Uint::try_from(&key.y)?; + let p = Uint::from(&key.p); + let q = Uint::from(&key.q); + let g = Uint::from(&key.g); + let y = Uint::from(&key.y); let components = dsa::Components::from_components(p, q, g)?; dsa::VerifyingKey::from_components(components, y).map_err(|_| Error::Crypto) @@ -137,20 +136,24 @@ impl TryFrom<&DsaPublicKey> for dsa::VerifyingKey { } #[cfg(feature = "dsa")] -impl From for DsaPublicKey { - fn from(key: dsa::VerifyingKey) -> DsaPublicKey { - DsaPublicKey::from(&key) +impl TryFrom for DsaPublicKey { + type Error = Error; + + fn try_from(key: dsa::VerifyingKey) -> Result { + DsaPublicKey::try_from(&key) } } #[cfg(feature = "dsa")] -impl From<&dsa::VerifyingKey> for DsaPublicKey { - fn from(key: &dsa::VerifyingKey) -> DsaPublicKey { - DsaPublicKey { - p: key.components().p().as_ref().into(), - q: key.components().q().as_ref().into(), - g: key.components().g().as_ref().into(), - y: key.y().as_ref().into(), - } +impl TryFrom<&dsa::VerifyingKey> for DsaPublicKey { + type Error = Error; + + fn try_from(key: &dsa::VerifyingKey) -> Result { + Self::new( + key.components().p().as_ref().try_into()?, + key.components().q().as_ref().try_into()?, + key.components().g().as_ref().try_into()?, + key.y().as_ref().try_into()?, + ) } } diff --git a/ssh-key/src/public/rsa.rs b/ssh-key/src/public/rsa.rs index d1444d9..b502c72 100644 --- a/ssh-key/src/public/rsa.rs +++ b/ssh-key/src/public/rsa.rs @@ -33,20 +33,13 @@ impl RsaPublicKey { /// - `n`: RSA modulus. /// /// # Errors - /// Returns [`Error::FormatEncoding`] in the event one of the components is odd. + /// Returns [`Error::FormatEncoding`] if either component is zero. pub fn new(e: Mpint, n: Mpint) -> Result { - if !e.is_positive() { + if e.is_zero() || n.is_zero() { return Err(Error::FormatEncoding); } - let bits = match n.as_positive_bytes() { - Some(bytes) => bytes - .len() - .checked_mul(8) - .and_then(|bits| u32::try_from(bits).ok()) - .ok_or(Error::FormatEncoding)?, - None => return Err(Error::FormatEncoding), - }; + let bits = u32::try_from(n.as_bytes().len() * 8).map_err(|_| Error::FormatEncoding)?; Ok(Self { e, n, bits }) } @@ -113,8 +106,8 @@ impl TryFrom<&RsaPublicKey> for rsa::PublicKey { type Error = Error; fn try_from(key: &RsaPublicKey) -> Result { - let n = Uint::try_from(&key.n)?; - let e = Uint::try_from(&key.e)?; + let n = Uint::from(&key.n); + let e = Uint::from(&key.e); let ret = rsa::PublicKey::new(n, e).map_err(|_| Error::Crypto)?; Ok(ret) @@ -135,7 +128,7 @@ impl TryFrom<&rsa::PublicKey> for RsaPublicKey { type Error = Error; fn try_from(key: &rsa::PublicKey) -> Result { - RsaPublicKey::new(key.e().into(), key.n().as_ref().into()) + RsaPublicKey::new(key.e().try_into()?, key.n().as_ref().try_into()?) } } diff --git a/ssh-key/src/signature.rs b/ssh-key/src/signature.rs index 72ba053..eaca86c 100644 --- a/ssh-key/src/signature.rs +++ b/ssh-key/src/signature.rs @@ -154,7 +154,10 @@ fn ecdsa_sig_size(data: &[u8], curve: EcdsaCurve, sk_trailer: bool) -> Result<() for _ in 0..2 { let component = Mpint::decode(&mut reader)?; - let bytes = component.as_positive_bytes().ok_or(Error::FormatEncoding)?; + if component.is_zero() { + return Err(Error::FormatEncoding); + } + let bytes = component.as_bytes(); if bytes.len() > curve.field_size() { return Err(encoding::Error::Length.into()); } @@ -522,8 +525,8 @@ macro_rules! impl_signature_for_curve { #[allow(clippy::arithmetic_side_effects)] let mut data = Vec::with_capacity($size * 2 + 4 * 2 + 2); - Mpint::from_positive_bytes(&r).encode(&mut data)?; - Mpint::from_positive_bytes(&s).encode(&mut data)?; + Mpint::from_bytes(&r)?.encode(&mut data)?; + Mpint::from_bytes(&s)?.encode(&mut data)?; Ok(Signature { algorithm: Algorithm::Ecdsa { @@ -563,7 +566,7 @@ impl_signature_for_curve!(p521, "p521", NistP521, 66); fn zero_pad_field_bytes + Copy>(m: Mpint) -> Option { use core::mem::size_of; - let bytes = m.as_positive_bytes()?; + let bytes = m.as_bytes(); size_of::().checked_sub(bytes.len()).map(|i| { core::iter::repeat_n(0u8, i) .chain(bytes.iter().cloned()) diff --git a/ssh-key/tests/certificate.rs b/ssh-key/tests/certificate.rs index c6f3d79..d9d2a10 100644 --- a/ssh-key/tests/certificate.rs +++ b/ssh-key/tests/certificate.rs @@ -72,14 +72,14 @@ fn decode_dsa_openssh() { let dsa_key = cert.public_key().dsa().unwrap(); assert_eq!( &hex!( - "00dc3d89250ed9462114cb2c8d4816e3a511aaff1b06b0e01de17c1cb04e581bcab97176471d89fd7ca1817 + "dc3d89250ed9462114cb2c8d4816e3a511aaff1b06b0e01de17c1cb04e581bcab97176471d89fd7ca1817 e3c48e2ccbafd2170f69e8e5c8b6ab69b9c5f45d95e1d9293e965227eee5b879b1123371c21b1db60f14b5e 5c05a4782ceb43a32f449647703063621e7a286bec95b16726c18b5e52383d00b297a6b03489b06068a5" ), dsa_key.p().as_bytes(), ); assert_eq!( - &hex!("00891815378597fe42d3fd261fe76df365845bbb87"), + &hex!("891815378597fe42d3fd261fe76df365845bbb87"), dsa_key.q().as_bytes(), ); assert_eq!( @@ -167,16 +167,11 @@ fn decode_rsa_4096_openssh() { assert_eq!(Algorithm::Rsa { hash: None }, cert.public_key().algorithm()); let rsa_key = cert.public_key().rsa().unwrap(); - dbg!(rsa_key.n().as_bytes()); - dbg!( - rsa_key.n().as_positive_bytes(), - rsa_key.n().as_positive_bytes().unwrap().len() - ); assert_eq!(4096, rsa_key.key_size()); assert_eq!(&hex!("010001"), rsa_key.e().as_bytes()); assert_eq!( &hex!( - "00b45911edc6ec5e7d2261a48c46ab889b1858306271123e6f02dc914cf3c0352492e8a6b7a7925added527 + "b45911edc6ec5e7d2261a48c46ab889b1858306271123e6f02dc914cf3c0352492e8a6b7a7925added527 e547dcebff6d0c19c0bc9153975199f47f4964ed20f5aceed4e82556b228a0c1fbfaa85e6339ba2ff4094d9 4e2b09d43a3dd68225d0bbc858293cbf167b18d6374ebe79220a633d400176f1f6b46fd626acb252bf294aa db2acd59626a023a8e5ec53ced8685164c72ca3a2ec646812c6e61ffcba740ff15c054f0691e3a8d52c79c4 diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index 5f252b9..3ec3c41 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -138,14 +138,14 @@ fn validate_dsa(key: PrivateKey) { let dsa_keypair = key.key_data().dsa().unwrap(); assert_eq!( &hex!( - "00dc3d89250ed9462114cb2c8d4816e3a511aaff1b06b0e01de17c1cb04e581bcab97176471d89fd7ca1817 + "dc3d89250ed9462114cb2c8d4816e3a511aaff1b06b0e01de17c1cb04e581bcab97176471d89fd7ca1817 e3c48e2ccbafd2170f69e8e5c8b6ab69b9c5f45d95e1d9293e965227eee5b879b1123371c21b1db60f14b5e 5c05a4782ceb43a32f449647703063621e7a286bec95b16726c18b5e52383d00b297a6b03489b06068a5" ), dsa_keypair.public().p().as_bytes(), ); assert_eq!( - &hex!("00891815378597fe42d3fd261fe76df365845bbb87"), + &hex!("891815378597fe42d3fd261fe76df365845bbb87"), dsa_keypair.public().q().as_bytes(), ); assert_eq!( @@ -418,7 +418,7 @@ fn validate_rsa_3072(key: PrivateKey) { assert_eq!(&hex!("010001"), rsa_keypair.public().e().as_bytes()); assert_eq!( &hex!( - "00a68e478c9bc93726436b7f5e9e6f9a46e1b73bec1e8cb7754de2c6a5b6c455f2f012a7259afcf94181d69 + "a68e478c9bc93726436b7f5e9e6f9a46e1b73bec1e8cb7754de2c6a5b6c455f2f012a7259afcf94181d69 e95d39a349e4d2b482a5372b28943731db75c73ce7bd9eec85010c94bfae56960118922f86a8b3655b357d2 4e7a679cd8a7d9bf6eae66f7f9a56fe3d090d0632218a682960d8aad93c01898780ead2dbefd70fb4703471 7e412e4fdae685292ec891e2423f7fe43df2f54329ab0a5d7561e582e42e86ebaee0c1e9eaf603d7ce70850 @@ -456,7 +456,7 @@ fn validate_rsa_3072(key: PrivateKey) { ); assert_eq!( &hex!( - "00d11e7ae248640d3b20293691886544cdfe0779a9aab56c8f9626bf1e6648bfd988bdb0ac7d11a73e34056 + "d11e7ae248640d3b20293691886544cdfe0779a9aab56c8f9626bf1e6648bfd988bdb0ac7d11a73e34056 336d4ad3900ff7184ee404b84baeb11a2a302f982d94eefb757030ec6d77d9115eaef80ab5749a0f5b590b2 99ffde94f9930dedbab2af333095b08a504b9b30f1112dce51f37ed00d043343d420d2e2fb88bd7f881b7c8 990e48b93fd1d284fcc8c1c07fbefc04d02925b2f159a9a9f567073e1c94fdc6e472f48963be16c5c545385 @@ -466,7 +466,7 @@ fn validate_rsa_3072(key: PrivateKey) { ); assert_eq!( &hex!( - "00cbe50e7ecdf5e2c86e76ca94be8b0c05b09041d2bcddeeb4672e7120eacc8380e5d6b77c85583c508f8ee + "cbe50e7ecdf5e2c86e76ca94be8b0c05b09041d2bcddeeb4672e7120eacc8380e5d6b77c85583c508f8ee 8d557cabcb6dfae8c72385f0f69345ebc15a5dfec949c0770d02d7945dd3592562da7388662e053c5454d26 90de2a63cc555e1a010e2da0ae307d1c11f852a9c7568e02f93d49b2a8f927b79943b2920cad321aa208410 9d136d085c1d05c39f4c36cf89fdc3c66a755e63f446e16302b13599400f0a83321a2e6b9153df02f03de31 @@ -491,7 +491,7 @@ fn decode_rsa_4096_openssh() { assert_eq!(&hex!("010001"), rsa_keypair.public().e().as_bytes()); assert_eq!( &hex!( - "00b45911edc6ec5e7d2261a48c46ab889b1858306271123e6f02dc914cf3c0352492e8a6b7a7925added527 + "b45911edc6ec5e7d2261a48c46ab889b1858306271123e6f02dc914cf3c0352492e8a6b7a7925added527 e547dcebff6d0c19c0bc9153975199f47f4964ed20f5aceed4e82556b228a0c1fbfaa85e6339ba2ff4094d9 4e2b09d43a3dd68225d0bbc858293cbf167b18d6374ebe79220a633d400176f1f6b46fd626acb252bf294aa db2acd59626a023a8e5ec53ced8685164c72ca3a2ec646812c6e61ffcba740ff15c054f0691e3a8d52c79c4 @@ -536,7 +536,7 @@ fn decode_rsa_4096_openssh() { ); assert_eq!( &hex!( - "00e2b7aa95621ec065acd1b9edd46090c715e1d212f11ac24f61c3016a4b411a25c635007654dc19a145531 + "e2b7aa95621ec065acd1b9edd46090c715e1d212f11ac24f61c3016a4b411a25c635007654dc19a145531 f9d49f796965a28f67575a5a9bf852b53474c4345cf604d40b614e31d50f0ca56414f152b49d6b92d8767d4 70a5a10afb6e546189d6e99739aeff7a081d96fd5c1646c5abbb8481df936c65aad51a553596e16f49b09f8 8175d2c938f92ecaccc61313523fd678533007f05cab51dfd16cfaf3439033bd3a4d845c08e34097b6f7ecd @@ -547,7 +547,7 @@ fn decode_rsa_4096_openssh() { ); assert_eq!( &hex!( - "00cba436332637becfdbdec249dbc121e5309c9964f2053c221b58b601846afd7cc8b788d6bf9b71345b1bd + "cba436332637becfdbdec249dbc121e5309c9964f2053c221b58b601846afd7cc8b788d6bf9b71345b1bd de7b367204e010ee60c2126352476ce98899b72035eb5f2ae8dd9754dd500354c418cbbf75dfd4bf2029a9a 3c8e097efdb334e8228a738b1c3fac43b4822364a54b4c348042369b59cf086b25db23226f71edeae58e77f c6f10493641c4254c28999be5628cd74e259d5fe5d39c98a9c0b8543bd58c89bb34ea19e18af714f1446e29 diff --git a/ssh-key/tests/public_key.rs b/ssh-key/tests/public_key.rs index 3e77739..06fc77a 100644 --- a/ssh-key/tests/public_key.rs +++ b/ssh-key/tests/public_key.rs @@ -68,14 +68,14 @@ fn decode_dsa_openssh() { let dsa_key = key.key_data().dsa().unwrap(); assert_eq!( &hex!( - "00dc3d89250ed9462114cb2c8d4816e3a511aaff1b06b0e01de17c1cb04e581bcab97176471d89fd7ca1817 + "dc3d89250ed9462114cb2c8d4816e3a511aaff1b06b0e01de17c1cb04e581bcab97176471d89fd7ca1817 e3c48e2ccbafd2170f69e8e5c8b6ab69b9c5f45d95e1d9293e965227eee5b879b1123371c21b1db60f14b5e 5c05a4782ceb43a32f449647703063621e7a286bec95b16726c18b5e52383d00b297a6b03489b06068a5" ), dsa_key.p().as_bytes(), ); assert_eq!( - &hex!("00891815378597fe42d3fd261fe76df365845bbb87"), + &hex!("891815378597fe42d3fd261fe76df365845bbb87"), dsa_key.q().as_bytes(), ); assert_eq!( @@ -225,7 +225,7 @@ fn decode_rsa_3072_openssh() { assert_eq!(&hex!("010001"), rsa_key.e().as_bytes()); assert_eq!( &hex!( - "00a68e478c9bc93726436b7f5e9e6f9a46e1b73bec1e8cb7754de2c6a5b6c455f2f012a7259afcf94181d69 + "a68e478c9bc93726436b7f5e9e6f9a46e1b73bec1e8cb7754de2c6a5b6c455f2f012a7259afcf94181d69 e95d39a349e4d2b482a5372b28943731db75c73ce7bd9eec85010c94bfae56960118922f86a8b3655b357d2 4e7a679cd8a7d9bf6eae66f7f9a56fe3d090d0632218a682960d8aad93c01898780ead2dbefd70fb4703471 7e412e4fdae685292ec891e2423f7fe43df2f54329ab0a5d7561e582e42e86ebaee0c1e9eaf603d7ce70850 @@ -255,7 +255,7 @@ fn decode_rsa_4096_openssh() { assert_eq!(&hex!("010001"), rsa_key.e().as_bytes()); assert_eq!( &hex!( - "00b45911edc6ec5e7d2261a48c46ab889b1858306271123e6f02dc914cf3c0352492e8a6b7a7925added527 + "b45911edc6ec5e7d2261a48c46ab889b1858306271123e6f02dc914cf3c0352492e8a6b7a7925added527 e547dcebff6d0c19c0bc9153975199f47f4964ed20f5aceed4e82556b228a0c1fbfaa85e6339ba2ff4094d9 4e2b09d43a3dd68225d0bbc858293cbf167b18d6374ebe79220a633d400176f1f6b46fd626acb252bf294aa db2acd59626a023a8e5ec53ced8685164c72ca3a2ec646812c6e61ffcba740ff15c054f0691e3a8d52c79c4