diff --git a/ssh-key/src/error.rs b/ssh-key/src/error.rs index 5a884d1e..d1729bd7 100644 --- a/ssh-key/src/error.rs +++ b/ssh-key/src/error.rs @@ -9,6 +9,9 @@ use crate::certificate; #[cfg(feature = "ppk")] use crate::ppk::PpkParseError; +#[cfg(doc)] +use crate::HashAlg; + /// Result type with `ssh-key`'s [`Error`] as the error type. pub type Result = core::result::Result; @@ -59,6 +62,9 @@ pub enum Error { /// Other format encoding errors. FormatEncoding, + /// Provided hash is the wrong size for a given [`HashAlg`]. + HashSize, + /// Input/output errors. #[cfg(feature = "std")] Io(std::io::ErrorKind), @@ -112,6 +118,7 @@ impl fmt::Display for Error { Error::Encoding(err) => write!(f, "{err}"), Error::Encrypted => write!(f, "private key is encrypted"), Error::FormatEncoding => write!(f, "format encoding error"), + Error::HashSize => write!(f, "hash is the wrong size for the given algorithm"), #[cfg(feature = "std")] Error::Io(err) => write!(f, "I/O error: {}", std::io::Error::from(*err)), Error::Namespace => write!(f, "namespace invalid"), diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index 13ebfdc8..1fdc212f 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -344,6 +344,21 @@ impl PrivateKey { SshSig::sign(self, namespace, hash_alg, msg) } + /// Sign the given message prehash using this private key, returning an [`SshSig`]. + /// + /// These signatures can be produced using `ssh-keygen -Y sign`. + /// + /// For more information, see [`PrivateKey::sign`]. + #[cfg(feature = "alloc")] + pub fn sign_prehash( + &self, + namespace: &str, + hash_alg: HashAlg, + prehash: &[u8], + ) -> Result { + SshSig::sign_prehash(self, namespace, hash_alg, prehash) + } + /// Read private key from an OpenSSH-formatted PEM source. #[cfg(feature = "std")] pub fn read_openssh(reader: &mut impl Read) -> Result { diff --git a/ssh-key/src/public.rs b/ssh-key/src/public.rs index 9088ec5b..14c6dcec 100644 --- a/ssh-key/src/public.rs +++ b/ssh-key/src/public.rs @@ -189,6 +189,12 @@ impl PublicKey { /// /// See [PROTOCOL.sshsig] for more information. /// + /// # Notes + /// + /// This method loads the entire message has to be loaded into memory for verification. + /// If loading the entire message into memory is a problem consider computing a [Digest] + /// of the data first, and using [`PublicKey::verify_prehash`]. + /// /// # Usage /// /// See also: [`PrivateKey::sign`]. @@ -224,14 +230,28 @@ impl PublicKey { /// # } /// ``` /// - /// The entire message has to be loaded into memory for verification. If loading the - /// entire message into memory is a problem consider computing a [Digest] via a - /// streaming API instead, and then signing/verifying a fixed length digest instead. - /// /// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD /// [Digest]: https://docs.rs/digest/latest/digest/trait.Digest.html #[cfg(feature = "alloc")] pub fn verify(&self, namespace: &str, msg: &[u8], signature: &SshSig) -> Result<()> { + self.verify_prehash( + namespace, + signature.hash_alg().digest(msg).as_slice(), + signature, + ) + } + + /// Verify the [`SshSig`] signature over the given prehashed message digest using this + /// public key. + /// + /// See [`PublicKey::verify`] for more information. + #[cfg(feature = "alloc")] + pub fn verify_prehash( + &self, + namespace: &str, + prehash: &[u8], + signature: &SshSig, + ) -> Result<()> { if self.key_data() != signature.public_key() { return Err(Error::PublicKey); } @@ -240,7 +260,7 @@ impl PublicKey { return Err(Error::Namespace); } - signature.verify(msg) + signature.verify_prehash(prehash) } /// Read public key from an OpenSSH-formatted source. diff --git a/ssh-key/src/sshsig.rs b/ssh-key/src/sshsig.rs index c2181c2a..57f893cc 100644 --- a/ssh-key/src/sshsig.rs +++ b/ssh-key/src/sshsig.rs @@ -106,6 +106,21 @@ impl SshSig { namespace: &str, hash_alg: HashAlg, msg: &[u8], + ) -> Result { + Self::sign_prehash( + signing_key, + namespace, + hash_alg, + hash_alg.digest(msg).as_slice(), + ) + } + + /// Sign the given prehashed message digest with the provided signing key. + pub fn sign_prehash( + signing_key: &S, + namespace: &str, + hash_alg: HashAlg, + prehash: &[u8], ) -> Result { if namespace.is_empty() { return Err(Error::Namespace); @@ -120,13 +135,13 @@ impl SshSig { return Err(Algorithm::SkEcdsaSha2NistP256.unsupported_error()); } - let signed_data = Self::signed_data(namespace, hash_alg, msg)?; + let signed_data = Self::signed_data_for_prehash(namespace, hash_alg, prehash)?; let signature = signing_key.try_sign(&signed_data)?; Self::new(signing_key.public_key(), namespace, hash_alg, signature) } - /// Get the raw message over which the signature for a given message - /// needs to be computed. + /// Get the raw "enveloped" message over which the signature for a given input message is + /// computed. /// /// This is a low-level function intended for uses cases which can't be /// expressed using [`SshSig::sign`], such as if the [`SigningKey`] trait @@ -135,6 +150,20 @@ impl SshSig { /// Once a [`Signature`] has been computed over the returned byte vector, /// [`SshSig::new`] can be used to construct the final signature. pub fn signed_data(namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result> { + Self::signed_data_for_prehash(namespace, hash_alg, hash_alg.digest(msg).as_slice()) + } + + /// Get the raw message over which the signature for a given message digest (passed as the + /// `prehash` parameter) is computed. + pub fn signed_data_for_prehash( + namespace: &str, + hash_alg: HashAlg, + prehash: &[u8], + ) -> Result> { + if prehash.len() != hash_alg.digest_size() { + return Err(Error::HashSize); + } + if namespace.is_empty() { return Err(Error::Namespace); } @@ -143,22 +172,26 @@ impl SshSig { namespace, reserved: &[], hash_alg, - hash: hash_alg.digest(msg).as_slice(), + hash: prehash, } .to_bytes() } - /// Verify the given message against this signature. + /// Verify the given prehashed message digest against this signature. /// /// Note that this method does not verify the public key or namespace /// are correct and thus is crate-private so as to ensure these parameters /// are always authenticated by users of the public API. - pub(crate) fn verify(&self, msg: &[u8]) -> Result<()> { + pub(crate) fn verify_prehash(&self, prehash: &[u8]) -> Result<()> { + if prehash.len() != self.hash_alg.digest_size() { + return Err(Error::HashSize); + } + let signed_data = SignedData { namespace: self.namespace.as_str(), reserved: self.reserved.as_slice(), hash_alg: self.hash_alg, - hash: self.hash_alg.digest(msg).as_slice(), + hash: prehash, } .to_bytes()?;