From edf9bc6c1411948b06287643e38686c048165af8 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Tue, 10 Nov 2026 11:58:50 -0700 Subject: [PATCH] ssh-encoding: non-negative `Mpint` and other OpenSSH alignment Restrict `Mpint` to non-negative integers and enforce the 16384-bit cap across every construction path. Match OpenSSH's rejection of negative encodings and acceptance of redundant leading zeros, checking the wire length before normalization and always emitting canonical encodings. Stores unsigned magnitudes internally and exposes them through `from_bytes` and `as_bytes`, removing the positive-specific APIs. Makes conversions from `crypto_bigint::Uint` fallible and conversions back infallible. Replaces derived bytewise ordering with numeric ordering using `ctutils`, and preserve canonical zero after zeroization. Updates `ssh-key` callers, including parsing PPK Ed25519 seeds as raw byte strings instead of mpints so their high bits and leading zeros are preserved. --- Cargo.toml | 1 - ssh-encoding/src/decoder.rs | 4 +- ssh-encoding/src/lib.rs | 15 +- ssh-encoding/src/mpint.rs | 576 +++++++++++++++++++++-------------- ssh-key/src/ppk.rs | 41 ++- ssh-key/src/private/dsa.rs | 69 ++--- ssh-key/src/private/rsa.rs | 28 +- ssh-key/src/public/dsa.rs | 43 +-- ssh-key/src/public/rsa.rs | 19 +- ssh-key/src/signature.rs | 11 +- ssh-key/tests/certificate.rs | 11 +- ssh-key/tests/private_key.rs | 16 +- ssh-key/tests/public_key.rs | 8 +- 13 files changed, 488 insertions(+), 354 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d8abef39..e2911f45 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 031d2429..ec9e3490 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 e85eeb2b..a3f70b0d 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 8e5fe63c..a14d23ee 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 835bc30e..c71b3697 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 7a440fe2..172625f8 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 f87f0e21..ceaa86b9 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 4a5d3d30..197a92a7 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 d1444d90..b502c72f 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 72ba0537..eaca86c1 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 c6f3d797..d9d2a109 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 5f252b97..3ec3c410 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 3e777393..06fc77aa 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