ssh_key/
public.rs

1//! SSH public key support.
2//!
3//! Support for decoding SSH public keys from the OpenSSH file format.
4
5#[cfg(feature = "alloc")]
6mod dsa;
7#[cfg(feature = "ecdsa")]
8mod ecdsa;
9mod ed25519;
10mod key_data;
11#[cfg(feature = "alloc")]
12mod opaque;
13#[cfg(feature = "alloc")]
14mod rsa;
15mod sk;
16mod ssh_format;
17
18pub use self::{ed25519::Ed25519PublicKey, key_data::KeyData, sk::SkEd25519};
19
20#[cfg(feature = "alloc")]
21pub use self::{
22    dsa::DsaPublicKey,
23    opaque::{OpaquePublicKey, OpaquePublicKeyBytes},
24    rsa::RsaPublicKey,
25};
26
27#[cfg(feature = "ecdsa")]
28pub use self::{ecdsa::EcdsaPublicKey, sk::SkEcdsaSha2NistP256};
29
30pub(crate) use self::ssh_format::SshFormat;
31
32use crate::{Algorithm, Error, Fingerprint, HashAlg, Result};
33use core::str::{self, FromStr};
34use encoding::{Base64Reader, Decode, Reader};
35
36#[cfg(feature = "alloc")]
37use {
38    crate::{AssociatedHashAlg, Comment, SshSig},
39    alloc::{
40        borrow::ToOwned,
41        string::{String, ToString},
42        vec::Vec,
43    },
44    encoding::Encode,
45    sha2::Digest,
46};
47
48#[cfg(all(feature = "alloc", feature = "serde"))]
49use serde::{Deserialize, Serialize, de, ser};
50
51#[cfg(feature = "std")]
52use std::{fs::File, path::Path};
53
54#[cfg(feature = "std")]
55use std::io::{self, Read, Write};
56
57#[cfg(doc)]
58use crate::PrivateKey;
59
60/// SSH public key.
61///
62/// # OpenSSH encoding
63///
64/// The OpenSSH encoding of an SSH public key looks like following:
65///
66/// ```text
67/// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com
68/// ```
69///
70/// It consists of the following three parts:
71///
72/// 1. Algorithm identifier (in this example `ssh-ed25519`)
73/// 2. Key data encoded as Base64
74/// 3. [`Comment`] (optional): arbitrary label describing a key. Usually an email address
75///
76/// The [`PublicKey::from_openssh`] and [`PublicKey::to_openssh`] methods can be
77/// used to decode/encode public keys, or alternatively, the [`FromStr`] and
78/// [`ToString`] impls.
79///
80/// # `serde` support
81///
82/// When the `serde` feature of this crate is enabled, this type receives impls
83/// of [`Deserialize`][`serde::Deserialize`] and [`Serialize`][`serde::Serialize`].
84///
85/// The serialization uses a binary encoding with binary formats like bincode
86/// and CBOR, and the OpenSSH string serialization when used with
87/// human-readable formats like JSON and TOML.
88///
89/// Note that since the `comment` is an artifact on the string serialization of
90/// a public key, it will be implicitly dropped when encoding as a binary
91/// format. To ensure it's always preserved even when using binary formats, you
92/// will first need to convert the [`PublicKey`] to a string using e.g.
93/// [`PublicKey::to_openssh`].
94#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
95pub struct PublicKey {
96    /// Key data.
97    pub(crate) key_data: KeyData,
98
99    /// Comment on the key (e.g. email address)
100    ///
101    /// Note that when a [`PublicKey`] is serialized in a private key, the
102    /// comment is encoded as an RFC4251 `string` which may contain arbitrary
103    /// binary data, so `Vec<u8>` is used to store the comment to ensure keys
104    /// containing such comments successfully round-trip.
105    #[cfg(feature = "alloc")]
106    pub(crate) comment: Comment,
107}
108
109impl PublicKey {
110    /// Create a new public key with the given comment.
111    ///
112    /// On `no_std` platforms, use `PublicKey::from(key_data)` instead.
113    #[cfg(feature = "alloc")]
114    pub fn new(key_data: KeyData, comment: impl Into<Comment>) -> Self {
115        Self {
116            key_data,
117            comment: comment.into(),
118        }
119    }
120
121    /// Parse an OpenSSH-formatted public key.
122    ///
123    /// OpenSSH-formatted public keys look like the following:
124    ///
125    /// ```text
126    /// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti foo@bar.com
127    /// ```
128    pub fn from_openssh(public_key: &str) -> Result<Self> {
129        let encapsulation = SshFormat::decode(public_key.trim_end().as_bytes())?;
130        let mut reader = Base64Reader::new(encapsulation.base64_data)?;
131        let key_data = KeyData::decode(&mut reader)?;
132
133        // Verify that the algorithm in the Base64-encoded data matches the text
134        if encapsulation.algorithm_id != key_data.algorithm().as_str() {
135            return Err(Error::AlgorithmUnknown);
136        }
137
138        let public_key = Self {
139            key_data,
140            #[cfg(feature = "alloc")]
141            comment: encapsulation.comment.to_owned().into(),
142        };
143
144        Ok(reader.finish(public_key)?)
145    }
146
147    /// Parse a raw binary SSH public key.
148    pub fn from_bytes(mut bytes: &[u8]) -> Result<Self> {
149        let reader = &mut bytes;
150        let key_data = KeyData::decode(reader)?;
151        Ok(reader.finish(key_data.into())?)
152    }
153
154    /// Encode OpenSSH-formatted public key.
155    pub fn encode_openssh<'o>(&self, out: &'o mut [u8]) -> Result<&'o str> {
156        #[cfg(not(feature = "alloc"))]
157        let comment = "";
158        #[cfg(feature = "alloc")]
159        let comment = self.comment.as_str_lossy();
160
161        SshFormat::encode(self.algorithm().as_str(), &self.key_data, comment, out)
162    }
163
164    /// Encode an OpenSSH-formatted public key, allocating a [`String`] for
165    /// the result.
166    #[cfg(feature = "alloc")]
167    pub fn to_openssh(&self) -> Result<String> {
168        SshFormat::encode_string(
169            self.algorithm().as_str(),
170            &self.key_data,
171            self.comment.as_str_lossy(),
172        )
173    }
174
175    /// Serialize SSH public key as raw bytes.
176    #[cfg(feature = "alloc")]
177    pub fn to_bytes(&self) -> Result<Vec<u8>> {
178        Ok(self.key_data.encode_vec()?)
179    }
180
181    /// Verify the [`SshSig`] signature is valid the given message using this public key.
182    ///
183    /// These signatures can be produced using `ssh-keygen -Y sign`. They're
184    /// encoded as PEM and begin with the following:
185    ///
186    /// ```text
187    /// -----BEGIN SSH SIGNATURE-----
188    /// ```
189    ///
190    /// See [PROTOCOL.sshsig] for more information.
191    ///
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    ///
198    /// # Usage
199    ///
200    /// See also: [`PrivateKey::sign`].
201    ///
202    #[cfg_attr(feature = "ed25519", doc = "```")]
203    #[cfg_attr(not(feature = "ed25519"), doc = "```ignore")]
204    /// # fn main() -> Result<(), ssh_key::Error> {
205    /// use ssh_key::{PublicKey, SshSig};
206    ///
207    /// // Message to be verified.
208    /// let message = b"testing";
209    ///
210    /// // Example domain/namespace used for the message.
211    /// let namespace = "example";
212    ///
213    /// // Public key which computed the signature.
214    /// let encoded_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com";
215    ///
216    /// // Example signature to be verified.
217    /// let signature_str = r#"
218    /// -----BEGIN SSH SIGNATURE-----
219    /// U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgsz6u836i33yqAQ3v3qNOJB9l8b
220    /// UppPQ+0UMn9cVKq2IAAAAHZXhhbXBsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQy
221    /// NTUxOQAAAEBPEav+tMGNnox4MuzM7rlHyVBajCn8B0kAyiOWwPKprNsG3i6X+voz/WCSik
222    /// /FowYwqhgCABUJSvRX3AERVBUP
223    /// -----END SSH SIGNATURE-----
224    /// "#;
225    ///
226    /// let public_key = encoded_public_key.parse::<PublicKey>()?;
227    /// let signature = signature_str.parse::<SshSig>()?;
228    /// public_key.verify(namespace, message, &signature)?;
229    /// # Ok(())
230    /// # }
231    /// ```
232    ///
233    /// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
234    /// [Digest]: https://docs.rs/digest/latest/digest/trait.Digest.html
235    #[cfg(feature = "alloc")]
236    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 is valid the given message [`Digest`] using this public key.
245    ///
246    /// See [`PublicKey::verify`] for more information.
247    #[cfg(feature = "alloc")]
248    pub fn verify_digest<D: AssociatedHashAlg + Digest>(
249        &self,
250        namespace: &str,
251        digest: D,
252        signature: &SshSig,
253    ) -> Result<()> {
254        if D::HASH_ALG != signature.hash_alg() {
255            return Err(Error::Crypto);
256        }
257
258        self.verify_prehash(namespace, digest.finalize().as_slice(), signature)
259    }
260
261    /// Verify the [`SshSig`] signature matches the given prehashed message digest using this
262    /// public key.
263    ///
264    /// See [`PublicKey::verify`] for more information.
265    #[cfg(feature = "alloc")]
266    pub fn verify_prehash(
267        &self,
268        namespace: &str,
269        prehash: &[u8],
270        signature: &SshSig,
271    ) -> Result<()> {
272        if self.key_data() != signature.public_key() {
273            return Err(Error::PublicKey);
274        }
275
276        if namespace != signature.namespace() {
277            return Err(Error::Namespace);
278        }
279
280        signature.verify_prehash(prehash)
281    }
282
283    /// Read public key from an OpenSSH-formatted source.
284    #[cfg(feature = "std")]
285    pub fn read_openssh(reader: &mut impl Read) -> Result<Self> {
286        let input = io::read_to_string(reader)?;
287        Self::from_openssh(&input)
288    }
289
290    /// Read public key from an OpenSSH-formatted file.
291    #[cfg(feature = "std")]
292    pub fn read_openssh_file(path: impl AsRef<Path>) -> Result<Self> {
293        let mut file = File::open(path)?;
294        Self::read_openssh(&mut file)
295    }
296
297    /// Write public key as an OpenSSH-formatted file.
298    #[cfg(feature = "std")]
299    pub fn write_openssh(&self, writer: &mut impl Write) -> Result<()> {
300        let mut encoded = self.to_openssh()?;
301        encoded.push('\n'); // TODO(tarcieri): OS-specific line endings?
302
303        writer.write_all(encoded.as_bytes())?;
304        Ok(())
305    }
306
307    /// Write public key as an OpenSSH-formatted file.
308    #[cfg(feature = "std")]
309    pub fn write_openssh_file(&self, path: impl AsRef<Path>) -> Result<()> {
310        let mut file = File::create(path)?;
311        self.write_openssh(&mut file)
312    }
313
314    /// Get the digital signature [`Algorithm`] used by this key.
315    pub fn algorithm(&self) -> Algorithm {
316        self.key_data.algorithm()
317    }
318
319    /// Comment on the key (e.g. email address).
320    #[cfg(feature = "alloc")]
321    pub fn comment(&self) -> &Comment {
322        &self.comment
323    }
324
325    /// Public key data.
326    pub fn key_data(&self) -> &KeyData {
327        &self.key_data
328    }
329
330    /// Compute key fingerprint.
331    ///
332    /// Use [`Default::default()`] to use the default hash function (SHA-256).
333    pub fn fingerprint(&self, hash_alg: HashAlg) -> Fingerprint {
334        self.key_data.fingerprint(hash_alg)
335    }
336
337    /// Set the comment on the key.
338    #[cfg(feature = "alloc")]
339    pub fn set_comment(&mut self, comment: impl Into<Comment>) {
340        self.comment = comment.into();
341    }
342
343    /// Decode comment (e.g. email address).
344    ///
345    /// This is a stub implementation that ignores the comment.
346    #[cfg(not(feature = "alloc"))]
347    pub(crate) fn decode_comment(&mut self, reader: &mut impl Reader) -> Result<()> {
348        reader.drain_prefixed()?;
349        Ok(())
350    }
351
352    /// Decode comment (e.g. email address)
353    #[cfg(feature = "alloc")]
354    pub(crate) fn decode_comment(&mut self, reader: &mut impl Reader) -> Result<()> {
355        self.comment = Comment::decode(reader)?;
356        Ok(())
357    }
358}
359
360impl From<KeyData> for PublicKey {
361    fn from(key_data: KeyData) -> PublicKey {
362        PublicKey {
363            key_data,
364            #[cfg(feature = "alloc")]
365            comment: Comment::default(),
366        }
367    }
368}
369
370impl From<PublicKey> for KeyData {
371    fn from(public_key: PublicKey) -> KeyData {
372        public_key.key_data
373    }
374}
375
376impl From<&PublicKey> for KeyData {
377    fn from(public_key: &PublicKey) -> KeyData {
378        public_key.key_data.clone()
379    }
380}
381
382#[cfg(feature = "alloc")]
383impl From<DsaPublicKey> for PublicKey {
384    fn from(public_key: DsaPublicKey) -> PublicKey {
385        KeyData::from(public_key).into()
386    }
387}
388
389#[cfg(feature = "ecdsa")]
390impl From<EcdsaPublicKey> for PublicKey {
391    fn from(public_key: EcdsaPublicKey) -> PublicKey {
392        KeyData::from(public_key).into()
393    }
394}
395
396impl From<Ed25519PublicKey> for PublicKey {
397    fn from(public_key: Ed25519PublicKey) -> PublicKey {
398        KeyData::from(public_key).into()
399    }
400}
401
402#[cfg(feature = "alloc")]
403impl From<RsaPublicKey> for PublicKey {
404    fn from(public_key: RsaPublicKey) -> PublicKey {
405        KeyData::from(public_key).into()
406    }
407}
408
409#[cfg(feature = "ecdsa")]
410impl From<SkEcdsaSha2NistP256> for PublicKey {
411    fn from(public_key: SkEcdsaSha2NistP256) -> PublicKey {
412        KeyData::from(public_key).into()
413    }
414}
415
416impl From<SkEd25519> for PublicKey {
417    fn from(public_key: SkEd25519) -> PublicKey {
418        KeyData::from(public_key).into()
419    }
420}
421
422impl FromStr for PublicKey {
423    type Err = Error;
424
425    fn from_str(s: &str) -> Result<Self> {
426        Self::from_openssh(s)
427    }
428}
429
430#[cfg(feature = "alloc")]
431#[allow(clippy::to_string_trait_impl)]
432impl ToString for PublicKey {
433    fn to_string(&self) -> String {
434        self.to_openssh().expect("SSH public key encoding error")
435    }
436}
437
438#[cfg(all(feature = "alloc", feature = "serde"))]
439impl<'de> Deserialize<'de> for PublicKey {
440    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
441    where
442        D: de::Deserializer<'de>,
443    {
444        if deserializer.is_human_readable() {
445            let string = String::deserialize(deserializer)?;
446            Self::from_openssh(&string).map_err(de::Error::custom)
447        } else {
448            let bytes = Vec::<u8>::deserialize(deserializer)?;
449            Self::from_bytes(&bytes).map_err(de::Error::custom)
450        }
451    }
452}
453
454#[cfg(all(feature = "alloc", feature = "serde"))]
455impl Serialize for PublicKey {
456    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
457    where
458        S: ser::Serializer,
459    {
460        if serializer.is_human_readable() {
461            self.to_openssh()
462                .map_err(ser::Error::custom)?
463                .serialize(serializer)
464        } else {
465            self.to_bytes()
466                .map_err(ser::Error::custom)?
467                .serialize(serializer)
468        }
469    }
470}