Skip to content

Commit 0d5202d

Browse files
authored
ssh-key: add SshSig prehash support (#384)
Adds basic support for signing and verifying raw message digests, which can be computed incrementally e.g. over a large file. Closes #380
1 parent 5a1ac38 commit 0d5202d

4 files changed

Lines changed: 87 additions & 12 deletions

File tree

ssh-key/src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use crate::certificate;
99
#[cfg(feature = "ppk")]
1010
use crate::ppk::PpkParseError;
1111

12+
#[cfg(doc)]
13+
use crate::HashAlg;
14+
1215
/// Result type with `ssh-key`'s [`Error`] as the error type.
1316
pub type Result<T> = core::result::Result<T, Error>;
1417

@@ -59,6 +62,9 @@ pub enum Error {
5962
/// Other format encoding errors.
6063
FormatEncoding,
6164

65+
/// Provided hash is the wrong size for a given [`HashAlg`].
66+
HashSize,
67+
6268
/// Input/output errors.
6369
#[cfg(feature = "std")]
6470
Io(std::io::ErrorKind),
@@ -112,6 +118,7 @@ impl fmt::Display for Error {
112118
Error::Encoding(err) => write!(f, "{err}"),
113119
Error::Encrypted => write!(f, "private key is encrypted"),
114120
Error::FormatEncoding => write!(f, "format encoding error"),
121+
Error::HashSize => write!(f, "hash is the wrong size for the given algorithm"),
115122
#[cfg(feature = "std")]
116123
Error::Io(err) => write!(f, "I/O error: {}", std::io::Error::from(*err)),
117124
Error::Namespace => write!(f, "namespace invalid"),

ssh-key/src/private.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,21 @@ impl PrivateKey {
344344
SshSig::sign(self, namespace, hash_alg, msg)
345345
}
346346

347+
/// Sign the given message prehash using this private key, returning an [`SshSig`].
348+
///
349+
/// These signatures can be produced using `ssh-keygen -Y sign`.
350+
///
351+
/// For more information, see [`PrivateKey::sign`].
352+
#[cfg(feature = "alloc")]
353+
pub fn sign_prehash(
354+
&self,
355+
namespace: &str,
356+
hash_alg: HashAlg,
357+
prehash: &[u8],
358+
) -> Result<SshSig> {
359+
SshSig::sign_prehash(self, namespace, hash_alg, prehash)
360+
}
361+
347362
/// Read private key from an OpenSSH-formatted PEM source.
348363
#[cfg(feature = "std")]
349364
pub fn read_openssh(reader: &mut impl Read) -> Result<Self> {

ssh-key/src/public.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ impl PublicKey {
189189
///
190190
/// See [PROTOCOL.sshsig] for more information.
191191
///
192+
/// # Notes
193+
///
194+
/// This method loads the entire message has to be loaded into memory for verification.
195+
/// If loading the entire message into memory is a problem consider computing a [Digest]
196+
/// of the data first, and using [`PublicKey::verify_prehash`].
197+
///
192198
/// # Usage
193199
///
194200
/// See also: [`PrivateKey::sign`].
@@ -224,14 +230,28 @@ impl PublicKey {
224230
/// # }
225231
/// ```
226232
///
227-
/// The entire message has to be loaded into memory for verification. If loading the
228-
/// entire message into memory is a problem consider computing a [Digest] via a
229-
/// streaming API instead, and then signing/verifying a fixed length digest instead.
230-
///
231233
/// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
232234
/// [Digest]: https://docs.rs/digest/latest/digest/trait.Digest.html
233235
#[cfg(feature = "alloc")]
234236
pub fn verify(&self, namespace: &str, msg: &[u8], signature: &SshSig) -> Result<()> {
237+
self.verify_prehash(
238+
namespace,
239+
signature.hash_alg().digest(msg).as_slice(),
240+
signature,
241+
)
242+
}
243+
244+
/// Verify the [`SshSig`] signature over the given prehashed message digest using this
245+
/// public key.
246+
///
247+
/// See [`PublicKey::verify`] for more information.
248+
#[cfg(feature = "alloc")]
249+
pub fn verify_prehash(
250+
&self,
251+
namespace: &str,
252+
prehash: &[u8],
253+
signature: &SshSig,
254+
) -> Result<()> {
235255
if self.key_data() != signature.public_key() {
236256
return Err(Error::PublicKey);
237257
}
@@ -240,7 +260,7 @@ impl PublicKey {
240260
return Err(Error::Namespace);
241261
}
242262

243-
signature.verify(msg)
263+
signature.verify_prehash(prehash)
244264
}
245265

246266
/// Read public key from an OpenSSH-formatted source.

ssh-key/src/sshsig.rs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ impl SshSig {
106106
namespace: &str,
107107
hash_alg: HashAlg,
108108
msg: &[u8],
109+
) -> Result<Self> {
110+
Self::sign_prehash(
111+
signing_key,
112+
namespace,
113+
hash_alg,
114+
hash_alg.digest(msg).as_slice(),
115+
)
116+
}
117+
118+
/// Sign the given prehashed message digest with the provided signing key.
119+
pub fn sign_prehash<S: SigningKey>(
120+
signing_key: &S,
121+
namespace: &str,
122+
hash_alg: HashAlg,
123+
prehash: &[u8],
109124
) -> Result<Self> {
110125
if namespace.is_empty() {
111126
return Err(Error::Namespace);
@@ -120,13 +135,13 @@ impl SshSig {
120135
return Err(Algorithm::SkEcdsaSha2NistP256.unsupported_error());
121136
}
122137

123-
let signed_data = Self::signed_data(namespace, hash_alg, msg)?;
138+
let signed_data = Self::signed_data_for_prehash(namespace, hash_alg, prehash)?;
124139
let signature = signing_key.try_sign(&signed_data)?;
125140
Self::new(signing_key.public_key(), namespace, hash_alg, signature)
126141
}
127142

128-
/// Get the raw message over which the signature for a given message
129-
/// needs to be computed.
143+
/// Get the raw "enveloped" message over which the signature for a given input message is
144+
/// computed.
130145
///
131146
/// This is a low-level function intended for uses cases which can't be
132147
/// expressed using [`SshSig::sign`], such as if the [`SigningKey`] trait
@@ -135,6 +150,20 @@ impl SshSig {
135150
/// Once a [`Signature`] has been computed over the returned byte vector,
136151
/// [`SshSig::new`] can be used to construct the final signature.
137152
pub fn signed_data(namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result<Vec<u8>> {
153+
Self::signed_data_for_prehash(namespace, hash_alg, hash_alg.digest(msg).as_slice())
154+
}
155+
156+
/// Get the raw message over which the signature for a given message digest (passed as the
157+
/// `prehash` parameter) is computed.
158+
pub fn signed_data_for_prehash(
159+
namespace: &str,
160+
hash_alg: HashAlg,
161+
prehash: &[u8],
162+
) -> Result<Vec<u8>> {
163+
if prehash.len() != hash_alg.digest_size() {
164+
return Err(Error::HashSize);
165+
}
166+
138167
if namespace.is_empty() {
139168
return Err(Error::Namespace);
140169
}
@@ -143,22 +172,26 @@ impl SshSig {
143172
namespace,
144173
reserved: &[],
145174
hash_alg,
146-
hash: hash_alg.digest(msg).as_slice(),
175+
hash: prehash,
147176
}
148177
.to_bytes()
149178
}
150179

151-
/// Verify the given message against this signature.
180+
/// Verify the given prehashed message digest against this signature.
152181
///
153182
/// Note that this method does not verify the public key or namespace
154183
/// are correct and thus is crate-private so as to ensure these parameters
155184
/// are always authenticated by users of the public API.
156-
pub(crate) fn verify(&self, msg: &[u8]) -> Result<()> {
185+
pub(crate) fn verify_prehash(&self, prehash: &[u8]) -> Result<()> {
186+
if prehash.len() != self.hash_alg.digest_size() {
187+
return Err(Error::HashSize);
188+
}
189+
157190
let signed_data = SignedData {
158191
namespace: self.namespace.as_str(),
159192
reserved: self.reserved.as_slice(),
160193
hash_alg: self.hash_alg,
161-
hash: self.hash_alg.digest(msg).as_slice(),
194+
hash: prehash,
162195
}
163196
.to_bytes()?;
164197

0 commit comments

Comments
 (0)