ssh_key/
sshsig.rs

1//! `sshsig` implementation.
2
3use crate::{public, Algorithm, Error, HashAlg, Result, Signature, SigningKey};
4use alloc::{string::String, string::ToString, vec::Vec};
5use core::str::FromStr;
6use encoding::{
7    pem::{LineEnding, PemLabel},
8    CheckedSum, Decode, DecodePem, Encode, EncodePem, Reader, Writer,
9};
10use signature::Verifier;
11
12#[cfg(doc)]
13use crate::{PrivateKey, PublicKey};
14
15type Version = u32;
16
17/// `sshsig` provides a general-purpose signature format based on SSH keys and
18/// wire formats.
19///
20/// These signatures can be produced using `ssh-keygen -Y sign`. They're
21/// encoded as PEM and begin with the following:
22///
23/// ```text
24/// -----BEGIN SSH SIGNATURE-----
25/// ```
26///
27/// See [PROTOCOL.sshsig] for more information.
28///
29/// # Usage
30///
31/// See [`PrivateKey::sign`] and [`PublicKey::verify`] for usage information.
32///
33/// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct SshSig {
36    version: Version,
37    public_key: public::KeyData,
38    namespace: String,
39    reserved: Vec<u8>,
40    hash_alg: HashAlg,
41    signature: Signature,
42}
43
44impl SshSig {
45    /// Supported version.
46    pub const VERSION: Version = 1;
47
48    /// The preamble is the six-byte sequence "SSHSIG".
49    ///
50    /// It is included to ensure that manual signatures can never be confused
51    /// with any message signed during SSH user or host authentication.
52    const MAGIC_PREAMBLE: &'static [u8] = b"SSHSIG";
53
54    /// Create a new signature with the given public key, namespace, hash
55    /// algorithm, and signature.
56    pub fn new(
57        public_key: public::KeyData,
58        namespace: impl Into<String>,
59        hash_alg: HashAlg,
60        signature: Signature,
61    ) -> Result<Self> {
62        let version = Self::VERSION;
63        let namespace = namespace.into();
64        let reserved = Vec::new();
65
66        if namespace.is_empty() {
67            return Err(Error::Namespace);
68        }
69
70        Ok(Self {
71            version,
72            public_key,
73            namespace,
74            reserved,
75            hash_alg,
76            signature,
77        })
78    }
79
80    /// Decode signature from PEM which begins with the following:
81    ///
82    /// ```text
83    /// -----BEGIN SSH SIGNATURE-----
84    /// ```
85    pub fn from_pem(pem: impl AsRef<[u8]>) -> Result<Self> {
86        Self::decode_pem(pem)
87    }
88
89    /// Encode signature as PEM which begins with the following:
90    ///
91    /// ```text
92    /// -----BEGIN SSH SIGNATURE-----
93    /// ```
94    pub fn to_pem(&self, line_ending: LineEnding) -> Result<String> {
95        Ok(self.encode_pem_string(line_ending)?)
96    }
97
98    /// Sign the given message with the provided signing key.
99    ///
100    /// See also: [`PrivateKey::sign`].
101    pub fn sign<S: SigningKey>(
102        signing_key: &S,
103        namespace: &str,
104        hash_alg: HashAlg,
105        msg: &[u8],
106    ) -> Result<Self> {
107        if namespace.is_empty() {
108            return Err(Error::Namespace);
109        }
110
111        if signing_key.public_key().is_sk_ed25519() {
112            return Err(Algorithm::SkEd25519.unsupported_error());
113        }
114
115        #[cfg(feature = "ecdsa")]
116        if signing_key.public_key().is_sk_ecdsa_p256() {
117            return Err(Algorithm::SkEcdsaSha2NistP256.unsupported_error());
118        }
119
120        let signed_data = Self::signed_data(namespace, hash_alg, msg)?;
121        let signature = signing_key.try_sign(&signed_data)?;
122        Self::new(signing_key.public_key(), namespace, hash_alg, signature)
123    }
124
125    /// Get the raw message over which the signature for a given message
126    /// needs to be computed.
127    ///
128    /// This is a low-level function intended for uses cases which can't be
129    /// expressed using [`SshSig::sign`], such as if the [`SigningKey`] trait
130    /// can't be used for some reason.
131    ///
132    /// Once a [`Signature`] has been computed over the returned byte vector,
133    /// [`SshSig::new`] can be used to construct the final signature.
134    pub fn signed_data(namespace: &str, hash_alg: HashAlg, msg: &[u8]) -> Result<Vec<u8>> {
135        if namespace.is_empty() {
136            return Err(Error::Namespace);
137        }
138
139        SignedData {
140            namespace,
141            reserved: &[],
142            hash_alg,
143            hash: hash_alg.digest(msg).as_slice(),
144        }
145        .to_bytes()
146    }
147
148    /// Verify the given message against this signature.
149    ///
150    /// Note that this method does not verify the public key or namespace
151    /// are correct and thus is crate-private so as to ensure these parameters
152    /// are always authenticated by users of the public API.
153    pub(crate) fn verify(&self, msg: &[u8]) -> Result<()> {
154        let signed_data = SignedData {
155            namespace: self.namespace.as_str(),
156            reserved: self.reserved.as_slice(),
157            hash_alg: self.hash_alg,
158            hash: self.hash_alg.digest(msg).as_slice(),
159        }
160        .to_bytes()?;
161
162        Ok(self.public_key.verify(&signed_data, &self.signature)?)
163    }
164
165    /// Get the signature algorithm.
166    pub fn algorithm(&self) -> Algorithm {
167        self.signature.algorithm()
168    }
169
170    /// Get version number for this signature.
171    ///
172    /// Verifiers MUST reject signatures with versions greater than those
173    /// they support.
174    pub fn version(&self) -> Version {
175        self.version
176    }
177
178    /// Get public key which corresponds to the signing key that produced
179    /// this signature.
180    pub fn public_key(&self) -> &public::KeyData {
181        &self.public_key
182    }
183
184    /// Get the namespace (i.e. domain identifier) for this signature.
185    ///
186    /// The purpose of the namespace value is to specify a unambiguous
187    /// interpretation domain for the signature, e.g. file signing.
188    /// This prevents cross-protocol attacks caused by signatures
189    /// intended for one intended domain being accepted in another.
190    /// The namespace value MUST NOT be the empty string.
191    pub fn namespace(&self) -> &str {
192        &self.namespace
193    }
194
195    /// Get reserved data associated with this signature. Typically empty.
196    ///
197    /// The reserved value is present to encode future information
198    /// (e.g. tags) into the signature. Implementations should ignore
199    /// the reserved field if it is not empty.
200    pub fn reserved(&self) -> &[u8] {
201        &self.reserved
202    }
203
204    /// Get the hash algorithm used to produce this signature.
205    ///
206    /// Data to be signed is first hashed with the specified `hash_alg`.
207    /// This is done to limit the amount of data presented to the signature
208    /// operation, which may be of concern if the signing key is held in limited
209    /// or slow hardware or on a remote ssh-agent. The supported hash algorithms
210    /// are "sha256" and "sha512".
211    pub fn hash_alg(&self) -> HashAlg {
212        self.hash_alg
213    }
214
215    /// Get the structured signature over the given message.
216    pub fn signature(&self) -> &Signature {
217        &self.signature
218    }
219
220    /// Get the bytes which comprise the serialized signature.
221    pub fn signature_bytes(&self) -> &[u8] {
222        self.signature.as_bytes()
223    }
224}
225
226impl Decode for SshSig {
227    type Error = Error;
228
229    fn decode(reader: &mut impl Reader) -> Result<Self> {
230        let mut magic_preamble = [0u8; Self::MAGIC_PREAMBLE.len()];
231        reader.read(&mut magic_preamble)?;
232
233        if magic_preamble != Self::MAGIC_PREAMBLE {
234            return Err(Error::FormatEncoding);
235        }
236
237        let version = Version::decode(reader)?;
238
239        if version > Self::VERSION {
240            return Err(Error::Version { number: version });
241        }
242
243        let public_key = reader.read_prefixed(public::KeyData::decode)?;
244        let namespace = String::decode(reader)?;
245
246        if namespace.is_empty() {
247            return Err(Error::Namespace);
248        }
249
250        let reserved = Vec::decode(reader)?;
251        let hash_alg = HashAlg::decode(reader)?;
252        let signature = reader.read_prefixed(Signature::decode)?;
253
254        Ok(Self {
255            version,
256            public_key,
257            namespace,
258            reserved,
259            hash_alg,
260            signature,
261        })
262    }
263}
264
265impl Encode for SshSig {
266    fn encoded_len(&self) -> encoding::Result<usize> {
267        [
268            Self::MAGIC_PREAMBLE.len(),
269            self.version.encoded_len()?,
270            self.public_key.encoded_len_prefixed()?,
271            self.namespace.encoded_len()?,
272            self.reserved.encoded_len()?,
273            self.hash_alg.encoded_len()?,
274            self.signature.encoded_len_prefixed()?,
275        ]
276        .checked_sum()
277    }
278
279    fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> {
280        writer.write(Self::MAGIC_PREAMBLE)?;
281        self.version.encode(writer)?;
282        self.public_key.encode_prefixed(writer)?;
283        self.namespace.encode(writer)?;
284        self.reserved.encode(writer)?;
285        self.hash_alg.encode(writer)?;
286        self.signature.encode_prefixed(writer)?;
287        Ok(())
288    }
289}
290
291impl FromStr for SshSig {
292    type Err = Error;
293
294    fn from_str(s: &str) -> Result<Self> {
295        Self::from_pem(s)
296    }
297}
298
299impl PemLabel for SshSig {
300    const PEM_LABEL: &'static str = "SSH SIGNATURE";
301}
302
303impl ToString for SshSig {
304    fn to_string(&self) -> String {
305        self.to_pem(LineEnding::default())
306            .expect("SSH signature encoding error")
307    }
308}
309
310/// Data to be signed.
311#[derive(Clone, Copy, Debug, Eq, PartialEq)]
312struct SignedData<'a> {
313    namespace: &'a str,
314    reserved: &'a [u8],
315    hash_alg: HashAlg,
316    hash: &'a [u8],
317}
318
319impl<'a> SignedData<'a> {
320    fn to_bytes(self) -> Result<Vec<u8>> {
321        let mut signed_bytes = Vec::with_capacity(self.encoded_len()?);
322        self.encode(&mut signed_bytes)?;
323        Ok(signed_bytes)
324    }
325}
326
327impl Encode for SignedData<'_> {
328    fn encoded_len(&self) -> encoding::Result<usize> {
329        [
330            SshSig::MAGIC_PREAMBLE.len(),
331            self.namespace.encoded_len()?,
332            self.reserved.encoded_len()?,
333            self.hash_alg.encoded_len()?,
334            self.hash.encoded_len()?,
335        ]
336        .checked_sum()
337    }
338
339    fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> {
340        writer.write(SshSig::MAGIC_PREAMBLE)?;
341        self.namespace.encode(writer)?;
342        self.reserved.encode(writer)?;
343        self.hash_alg.encode(writer)?;
344        self.hash.encode(writer)?;
345        Ok(())
346    }
347}