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