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;
11mod openssh;
12#[cfg(feature = "alloc")]
13mod rsa;
14mod sk;
15
16pub use self::{ed25519::Ed25519PublicKey, key_data::KeyData, sk::SkEd25519};
17
18#[cfg(feature = "alloc")]
19pub use self::{dsa::DsaPublicKey, rsa::RsaPublicKey};
20
21#[cfg(feature = "ecdsa")]
22pub use self::{ecdsa::EcdsaPublicKey, sk::SkEcdsaSha2NistP256};
23
24pub(crate) use self::openssh::Encapsulation;
25
26use crate::{
27    decode::Decode,
28    encode::Encode,
29    reader::{Base64Reader, Reader},
30    Algorithm, Error, Result,
31};
32use core::str::FromStr;
33
34#[cfg(feature = "alloc")]
35use {
36    crate::{checked::CheckedSum, writer::base64_len},
37    alloc::{
38        borrow::ToOwned,
39        string::{String, ToString},
40        vec::Vec,
41    },
42};
43
44#[cfg(feature = "fingerprint")]
45use crate::{Fingerprint, HashAlg};
46
47#[cfg(all(feature = "alloc", feature = "serde"))]
48use serde::{de, ser, Deserialize, Serialize};
49
50#[cfg(feature = "std")]
51use std::{fs, path::Path};
52
53/// SSH public key.
54///
55/// # OpenSSH encoding
56///
57/// The OpenSSH encoding of an SSH public key looks like following:
58///
59/// ```text
60/// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com
61/// ```
62///
63/// It consists of the following three parts:
64///
65/// 1. Algorithm identifier (in this example `ssh-ed25519`)
66/// 2. Key data encoded as Base64
67/// 3. Comment (optional): arbitrary label describing a key. Usually an email address
68///
69/// The [`PublicKey::from_openssh`] and [`PublicKey::to_openssh`] methods can be
70/// used to decode/encode public keys, or alternatively, the [`FromStr`] and
71/// [`ToString`] impls.
72///
73/// # `serde` support
74///
75/// When the `serde` feature of this crate is enabled, this type receives impls
76/// of [`Deserialize`][`serde::Deserialize`] and [`Serialize`][`serde::Serialize`].
77///
78/// The serialization uses a binary encoding with binary formats like bincode
79/// and CBOR, and the OpenSSH string serialization when used with
80/// human-readable formats like JSON and TOML.
81#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
82pub struct PublicKey {
83    /// Key data.
84    pub(crate) key_data: KeyData,
85
86    /// Comment on the key (e.g. email address)
87    #[cfg(feature = "alloc")]
88    pub(crate) comment: String,
89}
90
91impl PublicKey {
92    /// Create a new public key with the given comment.
93    ///
94    /// On `no_std` platforms, use `PublicKey::from(key_data)` instead.
95    #[cfg(feature = "alloc")]
96    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
97    pub fn new(key_data: KeyData, comment: impl Into<String>) -> Self {
98        Self {
99            key_data,
100            comment: comment.into(),
101        }
102    }
103
104    /// Parse an OpenSSH-formatted public key.
105    ///
106    /// OpenSSH-formatted public keys look like the following:
107    ///
108    /// ```text
109    /// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti foo@bar.com
110    /// ```
111    pub fn from_openssh(public_key: &str) -> Result<Self> {
112        let encapsulation = Encapsulation::decode(public_key.trim_end().as_bytes())?;
113        let mut reader = Base64Reader::new(encapsulation.base64_data)?;
114        let key_data = KeyData::decode(&mut reader)?;
115
116        // Verify that the algorithm in the Base64-encoded data matches the text
117        if encapsulation.algorithm_id != key_data.algorithm().as_str() {
118            return Err(Error::Algorithm);
119        }
120
121        let public_key = Self {
122            key_data,
123            #[cfg(feature = "alloc")]
124            comment: encapsulation.comment.to_owned(),
125        };
126
127        reader.finish(public_key)
128    }
129
130    /// Parse a raw binary SSH public key.
131    pub fn from_bytes(mut bytes: &[u8]) -> Result<Self> {
132        let reader = &mut bytes;
133        let key_data = KeyData::decode(reader)?;
134        reader.finish(key_data.into())
135    }
136
137    /// Encode OpenSSH-formatted public key.
138    pub fn encode_openssh<'o>(&self, out: &'o mut [u8]) -> Result<&'o str> {
139        Encapsulation::encode(out, self.algorithm().as_str(), self.comment(), |writer| {
140            self.key_data.encode(writer)
141        })
142    }
143
144    /// Encode an OpenSSH-formatted public key, allocating a [`String`] for
145    /// the result.
146    #[cfg(feature = "alloc")]
147    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
148    pub fn to_openssh(&self) -> Result<String> {
149        let encoded_len = [
150            2, // interstitial spaces
151            self.algorithm().as_str().len(),
152            base64_len(self.key_data.encoded_len()?),
153            self.comment.len(),
154        ]
155        .checked_sum()?;
156
157        let mut buf = vec![0u8; encoded_len];
158        let actual_len = self.encode_openssh(&mut buf)?.len();
159        buf.truncate(actual_len);
160        Ok(String::from_utf8(buf)?)
161    }
162
163    /// Serialize SSH public key as raw bytes.
164    #[cfg(feature = "alloc")]
165    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
166    pub fn to_bytes(&self) -> Result<Vec<u8>> {
167        let mut public_key_bytes = Vec::new();
168        self.key_data.encode(&mut public_key_bytes)?;
169        Ok(public_key_bytes)
170    }
171
172    /// Read public key from an OpenSSH-formatted file.
173    #[cfg(feature = "std")]
174    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
175    pub fn read_openssh_file(path: &Path) -> Result<Self> {
176        let input = fs::read_to_string(path)?;
177        Self::from_openssh(&*input)
178    }
179
180    /// Write public key as an OpenSSH-formatted file.
181    #[cfg(feature = "std")]
182    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
183    pub fn write_openssh_file(&self, path: &Path) -> Result<()> {
184        let encoded = self.to_openssh()?;
185        fs::write(path, encoded.as_bytes())?;
186        Ok(())
187    }
188
189    /// Get the digital signature [`Algorithm`] used by this key.
190    pub fn algorithm(&self) -> Algorithm {
191        self.key_data.algorithm()
192    }
193
194    /// Comment on the key (e.g. email address).
195    #[cfg(not(feature = "alloc"))]
196    pub fn comment(&self) -> &str {
197        ""
198    }
199
200    /// Comment on the key (e.g. email address).
201    #[cfg(feature = "alloc")]
202    pub fn comment(&self) -> &str {
203        &self.comment
204    }
205
206    /// Private key data.
207    pub fn key_data(&self) -> &KeyData {
208        &self.key_data
209    }
210
211    /// Compute key fingerprint.
212    ///
213    /// Use [`Default::default()`] to use the default hash function (SHA-256).
214    #[cfg(feature = "fingerprint")]
215    #[cfg_attr(docsrs, doc(cfg(feature = "fingerprint")))]
216    pub fn fingerprint(&self, hash_alg: HashAlg) -> Fingerprint {
217        self.key_data.fingerprint(hash_alg)
218    }
219
220    /// Set the comment on the key.
221    #[cfg(feature = "alloc")]
222    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
223    pub fn set_comment(&mut self, comment: impl Into<String>) {
224        self.comment = comment.into();
225    }
226
227    /// Decode comment (e.g. email address).
228    ///
229    /// This is a stub implementation that ignores the comment.
230    #[cfg(not(feature = "alloc"))]
231    pub(crate) fn decode_comment(&mut self, reader: &mut impl Reader) -> Result<()> {
232        reader.drain_prefixed()?;
233        Ok(())
234    }
235
236    /// Decode comment (e.g. email address)
237    #[cfg(feature = "alloc")]
238    pub(crate) fn decode_comment(&mut self, reader: &mut impl Reader) -> Result<()> {
239        self.comment = String::decode(reader)?;
240        Ok(())
241    }
242}
243
244impl From<KeyData> for PublicKey {
245    fn from(key_data: KeyData) -> PublicKey {
246        PublicKey {
247            key_data,
248            #[cfg(feature = "alloc")]
249            comment: String::new(),
250        }
251    }
252}
253
254impl From<PublicKey> for KeyData {
255    fn from(public_key: PublicKey) -> KeyData {
256        public_key.key_data
257    }
258}
259
260impl From<&PublicKey> for KeyData {
261    fn from(public_key: &PublicKey) -> KeyData {
262        public_key.key_data.clone()
263    }
264}
265
266#[cfg(feature = "alloc")]
267#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
268impl From<DsaPublicKey> for PublicKey {
269    fn from(public_key: DsaPublicKey) -> PublicKey {
270        KeyData::from(public_key).into()
271    }
272}
273
274#[cfg(feature = "ecdsa")]
275#[cfg_attr(docsrs, doc(cfg(feature = "ecdsa")))]
276impl From<EcdsaPublicKey> for PublicKey {
277    fn from(public_key: EcdsaPublicKey) -> PublicKey {
278        KeyData::from(public_key).into()
279    }
280}
281
282impl From<Ed25519PublicKey> for PublicKey {
283    fn from(public_key: Ed25519PublicKey) -> PublicKey {
284        KeyData::from(public_key).into()
285    }
286}
287
288#[cfg(feature = "alloc")]
289#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
290impl From<RsaPublicKey> for PublicKey {
291    fn from(public_key: RsaPublicKey) -> PublicKey {
292        KeyData::from(public_key).into()
293    }
294}
295
296#[cfg(feature = "ecdsa")]
297#[cfg_attr(docsrs, doc(cfg(feature = "ecdsa")))]
298impl From<SkEcdsaSha2NistP256> for PublicKey {
299    fn from(public_key: SkEcdsaSha2NistP256) -> PublicKey {
300        KeyData::from(public_key).into()
301    }
302}
303
304impl From<SkEd25519> for PublicKey {
305    fn from(public_key: SkEd25519) -> PublicKey {
306        KeyData::from(public_key).into()
307    }
308}
309
310impl FromStr for PublicKey {
311    type Err = Error;
312
313    fn from_str(s: &str) -> Result<Self> {
314        Self::from_openssh(s)
315    }
316}
317
318#[cfg(feature = "alloc")]
319impl ToString for PublicKey {
320    fn to_string(&self) -> String {
321        self.to_openssh().expect("SSH public key encoding error")
322    }
323}
324
325#[cfg(all(feature = "alloc", feature = "serde"))]
326#[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "serde"))))]
327impl<'de> Deserialize<'de> for PublicKey {
328    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
329    where
330        D: de::Deserializer<'de>,
331    {
332        if deserializer.is_human_readable() {
333            let string = String::deserialize(deserializer)?;
334            Self::from_openssh(&string).map_err(de::Error::custom)
335        } else {
336            let bytes = Vec::<u8>::deserialize(deserializer)?;
337            Self::from_bytes(&bytes).map_err(de::Error::custom)
338        }
339    }
340}
341
342#[cfg(all(feature = "alloc", feature = "serde"))]
343#[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "serde"))))]
344impl Serialize for PublicKey {
345    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
346    where
347        S: ser::Serializer,
348    {
349        if serializer.is_human_readable() {
350            self.to_openssh()
351                .map_err(ser::Error::custom)?
352                .serialize(serializer)
353        } else {
354            self.to_bytes()
355                .map_err(ser::Error::custom)?
356                .serialize(serializer)
357        }
358    }
359}