Skip to main content

ssh_key/
sshsig.rs

1//! `sshsig` implementation.
2
3use crate::{Algorithm, AssociatedHashAlg, Error, HashAlg, Result, Signature, SigningKey, public};
4use alloc::{string::String, string::ToString, vec::Vec};
5use core::str::FromStr;
6use encoding::{
7    CheckedSum, Decode, DecodePem, Encode, EncodePem, Reader, Writer,
8    pem::{LineEnding, PemLabel},
9};
10use sha2::Digest;
11use signature::Verifier;
12
13#[cfg(doc)]
14use crate::{PrivateKey, PublicKey};
15
16type Version = u32;
17
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize, de, ser};
20
21/// `sshsig` provides a general-purpose signature format based on SSH keys and
22/// wire formats.
23///
24/// These signatures can be produced using `ssh-keygen -Y sign`. They're
25/// encoded as PEM and begin with the following:
26///
27/// ```text
28/// -----BEGIN SSH SIGNATURE-----
29/// ```
30///
31/// See [PROTOCOL.sshsig] for more information.
32///
33/// # Usage
34///
35/// See [`PrivateKey::sign`] and [`PublicKey::verify`] for usage information.
36///
37/// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct SshSig {
40    version: Version,
41    public_key: public::KeyData,
42    namespace: String,
43    reserved: Vec<u8>,
44    hash_alg: HashAlg,
45    signature: Signature,
46}
47
48impl SshSig {
49    /// Supported version.
50    pub const VERSION: Version = 1;
51
52    /// The preamble is the six-byte sequence "SSHSIG".
53    ///
54    /// It is included to ensure that manual signatures can never be confused with any message
55    /// signed during SSH user or host authentication.
56    const MAGIC_PREAMBLE: &'static [u8] = b"SSHSIG";
57
58    /// Create a new signature with the given public key, namespace, hash algorithm, and signature.
59    ///
60    /// # Errors
61    /// Returns [`Error::Namespace`] if the namespace is empty.
62    pub fn new(
63        public_key: public::KeyData,
64        namespace: impl Into<String>,
65        hash_alg: HashAlg,
66        signature: Signature,
67    ) -> Result<Self> {
68        let version = Self::VERSION;
69        let namespace = namespace.into();
70        let reserved = Vec::new();
71
72        if namespace.is_empty() {
73            return Err(Error::Namespace);
74        }
75
76        Ok(Self {
77            version,
78            public_key,
79            namespace,
80            reserved,
81            hash_alg,
82            signature,
83        })
84    }
85
86    /// Decode signature from PEM which begins with the following:
87    ///
88    /// ```text
89    /// -----BEGIN SSH SIGNATURE-----
90    /// ```
91    ///
92    /// # Errors
93    /// Returns [`Error::Encoding`] in the event of an encoding error.
94    pub fn from_pem(pem: impl AsRef<[u8]>) -> Result<Self> {
95        Self::decode_pem(pem)
96    }
97
98    /// Encode signature as PEM which begins with the following:
99    ///
100    /// ```text
101    /// -----BEGIN SSH SIGNATURE-----
102    /// ```
103    ///
104    /// # Errors
105    /// Returns [`Error::Encoding`] in the event of an encoding error.
106    pub fn to_pem(&self, line_ending: LineEnding) -> Result<String> {
107        Ok(self.encode_pem_string(line_ending)?)
108    }
109
110    /// Sign the given message with the provided signing key.
111    ///
112    /// See also: [`PrivateKey::sign`].
113    ///
114    /// # Errors
115    /// - Returns [`Error::AlgorithmUnsupported`] if this signing key's algorithm is unsupported
116    ///   by this library with its enabled features.
117    /// - Returns [`Error::Crypto`] in the event of an error occurring in the cryptographic
118    ///   implementation.
119    /// - Returns [`Error::Namespace`] if the namespace is empty.
120    pub fn sign<S: SigningKey>(
121        signing_key: &S,
122        namespace: &str,
123        hash_alg: HashAlg,
124        msg: &[u8],
125    ) -> Result<Self> {
126        Self::sign_prehash(
127            signing_key,
128            namespace,
129            hash_alg,
130            hash_alg.digest(msg).as_slice(),
131        )
132    }
133
134    /// Sign the given message digest with the provided signing key.
135    ///
136    /// # Errors
137    /// - Returns [`Error::AlgorithmUnsupported`] if this signing key's algorithm is unsupported
138    ///   by this library with its enabled features.
139    /// - Returns [`Error::Crypto`] in the event of an error occurring in the cryptographic
140    ///   implementation.
141    /// - Returns [`Error::Namespace`] if the namespace is empty.
142    pub fn sign_digest<S, D>(signing_key: &S, namespace: &str, digest: D) -> Result<Self>
143    where
144        S: SigningKey,
145        D: AssociatedHashAlg + Digest,
146    {
147        Self::sign_prehash(
148            signing_key,
149            namespace,
150            D::HASH_ALG,
151            digest.finalize().as_slice(),
152        )
153    }
154
155    /// Sign the given prehashed message digest with the provided signing key.
156    ///
157    /// # Errors
158    /// - Returns [`Error::AlgorithmUnsupported`] if this signing key's algorithm is unsupported
159    ///   by this library with its enabled features.
160    /// - Returns [`Error::Crypto`] in the event of an error occurring in the cryptographic
161    ///   implementation.
162    /// - Returns [`Error::Namespace`] if the namespace is empty.
163    pub fn sign_prehash<S: SigningKey>(
164        signing_key: &S,
165        namespace: &str,
166        hash_alg: HashAlg,
167        prehash: &[u8],
168    ) -> Result<Self> {
169        if namespace.is_empty() {
170            return Err(Error::Namespace);
171        }
172
173        if signing_key.public_key().is_sk_ed25519() {
174            return Err(Algorithm::SkEd25519.unsupported_error());
175        }
176
177        #[cfg(feature = "ecdsa")]
178        if signing_key.public_key().is_sk_ecdsa_p256() {
179            return Err(Algorithm::SkEcdsaSha2NistP256.unsupported_error());
180        }
181
182        let signed_data = Self::signed_data_for_prehash(namespace, hash_alg, prehash)?;
183        let signature = signing_key.try_sign(&signed_data)?;
184        Self::new(signing_key.public_key(), namespace, hash_alg, signature)
185    }
186
187    /// Get the raw "enveloped" message over which the signature for a given input message is
188    /// computed.
189    ///
190    /// This is a low-level function intended for uses cases which can't be expressed using
191    /// [`SshSig::sign`], such as if the [`SigningKey`] trait can't be used for some reason.
192    ///
193    /// Once a [`Signature`] has been computed over the returned byte vector, [`SshSig::new`] can be
194    /// used to construct the final signature.
195    ///
196    /// # Errors
197    /// Returns [`Error::Namespace`] if the namespace is empty.
198    pub fn signed_data(namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result<Vec<u8>> {
199        Self::signed_data_for_prehash(namespace, hash_alg, hash_alg.digest(msg).as_slice())
200    }
201
202    /// Get the raw message over which the signature for a given message digest (passed as the
203    /// `prehash` parameter) is computed.
204    ///
205    /// # Errors
206    /// - Returns [`Error::HashSize`] if `prehash` is not the right length for `hash_alg`.
207    /// - Returns [`Error::Namespace`] if the namespace is empty.
208    pub fn signed_data_for_prehash(
209        namespace: &str,
210        hash_alg: HashAlg,
211        prehash: &[u8],
212    ) -> Result<Vec<u8>> {
213        if prehash.len() != hash_alg.digest_size() {
214            return Err(Error::HashSize);
215        }
216
217        if namespace.is_empty() {
218            return Err(Error::Namespace);
219        }
220
221        SignedData {
222            namespace,
223            reserved: &[],
224            hash_alg,
225            hash: prehash,
226        }
227        .to_bytes()
228    }
229
230    /// Verify the given prehashed message digest against this signature.
231    ///
232    /// Note that this method does not verify the public key or namespace are correct and thus is
233    /// crate-private so as to ensure these parameters are always authenticated by users of the
234    /// public API.
235    pub(crate) fn verify_prehash(&self, prehash: &[u8]) -> Result<()> {
236        if prehash.len() != self.hash_alg.digest_size() {
237            return Err(Error::HashSize);
238        }
239
240        let signed_data = SignedData {
241            namespace: self.namespace.as_str(),
242            reserved: self.reserved.as_slice(),
243            hash_alg: self.hash_alg,
244            hash: prehash,
245        }
246        .to_bytes()?;
247
248        Ok(self.public_key.verify(&signed_data, &self.signature)?)
249    }
250
251    /// Get the signature algorithm.
252    #[must_use]
253    pub fn algorithm(&self) -> Algorithm {
254        self.signature.algorithm()
255    }
256
257    /// Get version number for this signature.
258    ///
259    /// Verifiers MUST reject signatures with versions greater than those
260    /// they support.
261    #[must_use]
262    pub fn version(&self) -> Version {
263        self.version
264    }
265
266    /// Get public key which corresponds to the signing key that produced
267    /// this signature.
268    #[must_use]
269    pub fn public_key(&self) -> &public::KeyData {
270        &self.public_key
271    }
272
273    /// Get the namespace (i.e. domain identifier) for this signature.
274    ///
275    /// The purpose of the namespace value is to specify a unambiguous
276    /// interpretation domain for the signature, e.g. file signing.
277    /// This prevents cross-protocol attacks caused by signatures
278    /// intended for one intended domain being accepted in another.
279    /// The namespace value MUST NOT be the empty string.
280    #[must_use]
281    pub fn namespace(&self) -> &str {
282        &self.namespace
283    }
284
285    /// Get reserved data associated with this signature. Typically empty.
286    ///
287    /// The reserved value is present to encode future information
288    /// (e.g. tags) into the signature. Implementations should ignore
289    /// the reserved field if it is not empty.
290    #[must_use]
291    pub fn reserved(&self) -> &[u8] {
292        &self.reserved
293    }
294
295    /// Get the hash algorithm used to produce this signature.
296    ///
297    /// Data to be signed is first hashed with the specified `hash_alg`.
298    /// This is done to limit the amount of data presented to the signature
299    /// operation, which may be of concern if the signing key is held in limited
300    /// or slow hardware or on a remote ssh-agent. The supported hash algorithms
301    /// are "sha256" and "sha512".
302    #[must_use]
303    pub fn hash_alg(&self) -> HashAlg {
304        self.hash_alg
305    }
306
307    /// Get the structured signature over the given message.
308    #[must_use]
309    pub fn signature(&self) -> &Signature {
310        &self.signature
311    }
312
313    /// Get the bytes which comprise the serialized signature.
314    #[must_use]
315    pub fn signature_bytes(&self) -> &[u8] {
316        self.signature.as_bytes()
317    }
318}
319
320impl Decode for SshSig {
321    type Error = Error;
322
323    fn decode(reader: &mut impl Reader) -> Result<Self> {
324        let mut magic_preamble = [0u8; Self::MAGIC_PREAMBLE.len()];
325        reader.read(&mut magic_preamble)?;
326
327        if magic_preamble != Self::MAGIC_PREAMBLE {
328            return Err(Error::FormatEncoding);
329        }
330
331        let version = Version::decode(reader)?;
332
333        if version > Self::VERSION {
334            return Err(Error::Version { number: version });
335        }
336
337        let public_key = reader.read_prefixed(public::KeyData::decode)?;
338        let namespace = String::decode(reader)?;
339
340        if namespace.is_empty() {
341            return Err(Error::Namespace);
342        }
343
344        let reserved = Vec::decode(reader)?;
345        let hash_alg = HashAlg::decode(reader)?;
346        let signature = reader.read_prefixed(Signature::decode)?;
347
348        Ok(Self {
349            version,
350            public_key,
351            namespace,
352            reserved,
353            hash_alg,
354            signature,
355        })
356    }
357}
358
359impl Encode for SshSig {
360    fn encoded_len(&self) -> encoding::Result<usize> {
361        [
362            Self::MAGIC_PREAMBLE.len(),
363            self.version.encoded_len()?,
364            self.public_key.encoded_len_prefixed()?,
365            self.namespace.encoded_len()?,
366            self.reserved.encoded_len()?,
367            self.hash_alg.encoded_len()?,
368            self.signature.encoded_len_prefixed()?,
369        ]
370        .checked_sum()
371    }
372
373    fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> {
374        writer.write(Self::MAGIC_PREAMBLE)?;
375        self.version.encode(writer)?;
376        self.public_key.encode_prefixed(writer)?;
377        self.namespace.encode(writer)?;
378        self.reserved.encode(writer)?;
379        self.hash_alg.encode(writer)?;
380        self.signature.encode_prefixed(writer)?;
381        Ok(())
382    }
383}
384
385impl FromStr for SshSig {
386    type Err = Error;
387
388    fn from_str(s: &str) -> Result<Self> {
389        Self::from_pem(s)
390    }
391}
392
393impl PemLabel for SshSig {
394    const PEM_LABEL: &'static str = "SSH SIGNATURE";
395}
396
397#[allow(clippy::to_string_trait_impl)]
398impl ToString for SshSig {
399    fn to_string(&self) -> String {
400        self.to_pem(LineEnding::default())
401            .expect("SSH signature encoding error")
402    }
403}
404
405/// Data to be signed.
406#[derive(Clone, Copy, Debug, Eq, PartialEq)]
407struct SignedData<'a> {
408    namespace: &'a str,
409    reserved: &'a [u8],
410    hash_alg: HashAlg,
411    hash: &'a [u8],
412}
413
414impl SignedData<'_> {
415    fn to_bytes(self) -> Result<Vec<u8>> {
416        let mut signed_bytes = Vec::with_capacity(self.encoded_len()?);
417        self.encode(&mut signed_bytes)?;
418        Ok(signed_bytes)
419    }
420}
421
422impl Encode for SignedData<'_> {
423    fn encoded_len(&self) -> encoding::Result<usize> {
424        [
425            SshSig::MAGIC_PREAMBLE.len(),
426            self.namespace.encoded_len()?,
427            self.reserved.encoded_len()?,
428            self.hash_alg.encoded_len()?,
429            self.hash.encoded_len()?,
430        ]
431        .checked_sum()
432    }
433
434    fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> {
435        writer.write(SshSig::MAGIC_PREAMBLE)?;
436        self.namespace.encode(writer)?;
437        self.reserved.encode(writer)?;
438        self.hash_alg.encode(writer)?;
439        self.hash.encode(writer)?;
440        Ok(())
441    }
442}
443
444#[cfg(feature = "serde")]
445impl<'de> Deserialize<'de> for SshSig {
446    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
447    where
448        D: de::Deserializer<'de>,
449    {
450        if deserializer.is_human_readable() {
451            let string = String::deserialize(deserializer)?;
452            string.parse::<SshSig>().map_err(de::Error::custom)
453        } else {
454            let bytes = Vec::<u8>::deserialize(deserializer)?;
455            Self::decode(&mut bytes.as_slice()).map_err(de::Error::custom)
456        }
457    }
458}
459
460#[cfg(feature = "serde")]
461impl Serialize for SshSig {
462    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
463    where
464        S: ser::Serializer,
465    {
466        if serializer.is_human_readable() {
467            self.to_pem(LineEnding::LF)
468                .map_err(ser::Error::custom)?
469                .serialize(serializer)
470        } else {
471            let bytes = self.encode_vec().map_err(ser::Error::custom)?;
472            bytes.serialize(serializer)
473        }
474    }
475}