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::FromStr;
34use encoding::{Base64Reader, Decode, Reader};
35
36#[cfg(feature = "alloc")]
37use {
38    crate::SshSig,
39    alloc::{
40        borrow::ToOwned,
41        string::{String, ToString},
42        vec::Vec,
43    },
44    encoding::Encode,
45};
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#[cfg(doc)]
54use crate::PrivateKey;
55
56/// SSH public key.
57///
58/// # OpenSSH encoding
59///
60/// The OpenSSH encoding of an SSH public key looks like following:
61///
62/// ```text
63/// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com
64/// ```
65///
66/// It consists of the following three parts:
67///
68/// 1. Algorithm identifier (in this example `ssh-ed25519`)
69/// 2. Key data encoded as Base64
70/// 3. Comment (optional): arbitrary label describing a key. Usually an email address
71///
72/// The [`PublicKey::from_openssh`] and [`PublicKey::to_openssh`] methods can be
73/// used to decode/encode public keys, or alternatively, the [`FromStr`] and
74/// [`ToString`] impls.
75///
76/// # `serde` support
77///
78/// When the `serde` feature of this crate is enabled, this type receives impls
79/// of [`Deserialize`][`serde::Deserialize`] and [`Serialize`][`serde::Serialize`].
80///
81/// The serialization uses a binary encoding with binary formats like bincode
82/// and CBOR, and the OpenSSH string serialization when used with
83/// human-readable formats like JSON and TOML.
84#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
85pub struct PublicKey {
86    /// Key data.
87    pub(crate) key_data: KeyData,
88
89    /// Comment on the key (e.g. email address)
90    #[cfg(feature = "alloc")]
91    pub(crate) comment: String,
92}
93
94impl PublicKey {
95    /// Create a new public key with the given comment.
96    ///
97    /// On `no_std` platforms, use `PublicKey::from(key_data)` instead.
98    #[cfg(feature = "alloc")]
99    pub fn new(key_data: KeyData, comment: impl Into<String>) -> Self {
100        Self {
101            key_data,
102            comment: comment.into(),
103        }
104    }
105
106    /// Parse an OpenSSH-formatted public key.
107    ///
108    /// OpenSSH-formatted public keys look like the following:
109    ///
110    /// ```text
111    /// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti foo@bar.com
112    /// ```
113    pub fn from_openssh(public_key: &str) -> Result<Self> {
114        let encapsulation = SshFormat::decode(public_key.trim_end().as_bytes())?;
115        let mut reader = Base64Reader::new(encapsulation.base64_data)?;
116        let key_data = KeyData::decode(&mut reader)?;
117
118        // Verify that the algorithm in the Base64-encoded data matches the text
119        if encapsulation.algorithm_id != key_data.algorithm().as_str() {
120            return Err(Error::AlgorithmUnknown);
121        }
122
123        let public_key = Self {
124            key_data,
125            #[cfg(feature = "alloc")]
126            comment: encapsulation.comment.to_owned(),
127        };
128
129        Ok(reader.finish(public_key)?)
130    }
131
132    /// Parse a raw binary SSH public key.
133    pub fn from_bytes(mut bytes: &[u8]) -> Result<Self> {
134        let reader = &mut bytes;
135        let key_data = KeyData::decode(reader)?;
136        Ok(reader.finish(key_data.into())?)
137    }
138
139    /// Encode OpenSSH-formatted public key.
140    pub fn encode_openssh<'o>(&self, out: &'o mut [u8]) -> Result<&'o str> {
141        SshFormat::encode(
142            self.algorithm().as_str(),
143            &self.key_data,
144            self.comment(),
145            out,
146        )
147    }
148
149    /// Encode an OpenSSH-formatted public key, allocating a [`String`] for
150    /// the result.
151    #[cfg(feature = "alloc")]
152    pub fn to_openssh(&self) -> Result<String> {
153        SshFormat::encode_string(self.algorithm().as_str(), &self.key_data, self.comment())
154    }
155
156    /// Serialize SSH public key as raw bytes.
157    #[cfg(feature = "alloc")]
158    pub fn to_bytes(&self) -> Result<Vec<u8>> {
159        let mut public_key_bytes = Vec::new();
160        self.key_data.encode(&mut public_key_bytes)?;
161        Ok(public_key_bytes)
162    }
163
164    /// Verify the [`SshSig`] signature over the given message using this
165    /// public key.
166    ///
167    /// These signatures can be produced using `ssh-keygen -Y sign`. They're
168    /// encoded as PEM and begin with the following:
169    ///
170    /// ```text
171    /// -----BEGIN SSH SIGNATURE-----
172    /// ```
173    ///
174    /// See [PROTOCOL.sshsig] for more information.
175    ///
176    /// # Usage
177    ///
178    /// See also: [`PrivateKey::sign`].
179    ///
180    #[cfg_attr(feature = "ed25519", doc = "```")]
181    #[cfg_attr(not(feature = "ed25519"), doc = "```ignore")]
182    /// # fn main() -> Result<(), ssh_key::Error> {
183    /// use ssh_key::{PublicKey, SshSig};
184    ///
185    /// // Message to be verified.
186    /// let message = b"testing";
187    ///
188    /// // Example domain/namespace used for the message.
189    /// let namespace = "example";
190    ///
191    /// // Public key which computed the signature.
192    /// let encoded_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com";
193    ///
194    /// // Example signature to be verified.
195    /// let signature_str = r#"
196    /// -----BEGIN SSH SIGNATURE-----
197    /// U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgsz6u836i33yqAQ3v3qNOJB9l8b
198    /// UppPQ+0UMn9cVKq2IAAAAHZXhhbXBsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQy
199    /// NTUxOQAAAEBPEav+tMGNnox4MuzM7rlHyVBajCn8B0kAyiOWwPKprNsG3i6X+voz/WCSik
200    /// /FowYwqhgCABUJSvRX3AERVBUP
201    /// -----END SSH SIGNATURE-----
202    /// "#;
203    ///
204    /// let public_key = encoded_public_key.parse::<PublicKey>()?;
205    /// let signature = signature_str.parse::<SshSig>()?;
206    /// public_key.verify(namespace, message, &signature)?;
207    /// # Ok(())
208    /// # }
209    /// ```
210    ///
211    /// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
212    #[cfg(feature = "alloc")]
213    pub fn verify(&self, namespace: &str, msg: &[u8], signature: &SshSig) -> Result<()> {
214        if self.key_data() != signature.public_key() {
215            return Err(Error::PublicKey);
216        }
217
218        if namespace != signature.namespace() {
219            return Err(Error::Namespace);
220        }
221
222        signature.verify(msg)
223    }
224
225    /// Read public key from an OpenSSH-formatted file.
226    #[cfg(feature = "std")]
227    pub fn read_openssh_file(path: &Path) -> Result<Self> {
228        let input = fs::read_to_string(path)?;
229        Self::from_openssh(&input)
230    }
231
232    /// Write public key as an OpenSSH-formatted file.
233    #[cfg(feature = "std")]
234    pub fn write_openssh_file(&self, path: &Path) -> Result<()> {
235        let mut encoded = self.to_openssh()?;
236        encoded.push('\n'); // TODO(tarcieri): OS-specific line endings?
237
238        fs::write(path, encoded.as_bytes())?;
239        Ok(())
240    }
241
242    /// Get the digital signature [`Algorithm`] used by this key.
243    pub fn algorithm(&self) -> Algorithm {
244        self.key_data.algorithm()
245    }
246
247    /// Comment on the key (e.g. email address).
248    #[cfg(not(feature = "alloc"))]
249    pub fn comment(&self) -> &str {
250        ""
251    }
252
253    /// Comment on the key (e.g. email address).
254    #[cfg(feature = "alloc")]
255    pub fn comment(&self) -> &str {
256        &self.comment
257    }
258
259    /// Public key data.
260    pub fn key_data(&self) -> &KeyData {
261        &self.key_data
262    }
263
264    /// Compute key fingerprint.
265    ///
266    /// Use [`Default::default()`] to use the default hash function (SHA-256).
267    pub fn fingerprint(&self, hash_alg: HashAlg) -> Fingerprint {
268        self.key_data.fingerprint(hash_alg)
269    }
270
271    /// Set the comment on the key.
272    #[cfg(feature = "alloc")]
273    pub fn set_comment(&mut self, comment: impl Into<String>) {
274        self.comment = comment.into();
275    }
276
277    /// Decode comment (e.g. email address).
278    ///
279    /// This is a stub implementation that ignores the comment.
280    #[cfg(not(feature = "alloc"))]
281    pub(crate) fn decode_comment(&mut self, reader: &mut impl Reader) -> Result<()> {
282        reader.drain_prefixed()?;
283        Ok(())
284    }
285
286    /// Decode comment (e.g. email address)
287    #[cfg(feature = "alloc")]
288    pub(crate) fn decode_comment(&mut self, reader: &mut impl Reader) -> Result<()> {
289        self.comment = String::decode(reader)?;
290        Ok(())
291    }
292}
293
294impl From<KeyData> for PublicKey {
295    fn from(key_data: KeyData) -> PublicKey {
296        PublicKey {
297            key_data,
298            #[cfg(feature = "alloc")]
299            comment: String::new(),
300        }
301    }
302}
303
304impl From<PublicKey> for KeyData {
305    fn from(public_key: PublicKey) -> KeyData {
306        public_key.key_data
307    }
308}
309
310impl From<&PublicKey> for KeyData {
311    fn from(public_key: &PublicKey) -> KeyData {
312        public_key.key_data.clone()
313    }
314}
315
316#[cfg(feature = "alloc")]
317impl From<DsaPublicKey> for PublicKey {
318    fn from(public_key: DsaPublicKey) -> PublicKey {
319        KeyData::from(public_key).into()
320    }
321}
322
323#[cfg(feature = "ecdsa")]
324impl From<EcdsaPublicKey> for PublicKey {
325    fn from(public_key: EcdsaPublicKey) -> PublicKey {
326        KeyData::from(public_key).into()
327    }
328}
329
330impl From<Ed25519PublicKey> for PublicKey {
331    fn from(public_key: Ed25519PublicKey) -> PublicKey {
332        KeyData::from(public_key).into()
333    }
334}
335
336#[cfg(feature = "alloc")]
337impl From<RsaPublicKey> for PublicKey {
338    fn from(public_key: RsaPublicKey) -> PublicKey {
339        KeyData::from(public_key).into()
340    }
341}
342
343#[cfg(feature = "ecdsa")]
344impl From<SkEcdsaSha2NistP256> for PublicKey {
345    fn from(public_key: SkEcdsaSha2NistP256) -> PublicKey {
346        KeyData::from(public_key).into()
347    }
348}
349
350impl From<SkEd25519> for PublicKey {
351    fn from(public_key: SkEd25519) -> PublicKey {
352        KeyData::from(public_key).into()
353    }
354}
355
356impl FromStr for PublicKey {
357    type Err = Error;
358
359    fn from_str(s: &str) -> Result<Self> {
360        Self::from_openssh(s)
361    }
362}
363
364#[cfg(feature = "alloc")]
365impl ToString for PublicKey {
366    fn to_string(&self) -> String {
367        self.to_openssh().expect("SSH public key encoding error")
368    }
369}
370
371#[cfg(all(feature = "alloc", feature = "serde"))]
372impl<'de> Deserialize<'de> for PublicKey {
373    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
374    where
375        D: de::Deserializer<'de>,
376    {
377        if deserializer.is_human_readable() {
378            let string = String::deserialize(deserializer)?;
379            Self::from_openssh(&string).map_err(de::Error::custom)
380        } else {
381            let bytes = Vec::<u8>::deserialize(deserializer)?;
382            Self::from_bytes(&bytes).map_err(de::Error::custom)
383        }
384    }
385}
386
387#[cfg(all(feature = "alloc", feature = "serde"))]
388impl Serialize for PublicKey {
389    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
390    where
391        S: ser::Serializer,
392    {
393        if serializer.is_human_readable() {
394            self.to_openssh()
395                .map_err(ser::Error::custom)?
396                .serialize(serializer)
397        } else {
398            self.to_bytes()
399                .map_err(ser::Error::custom)?
400                .serialize(serializer)
401        }
402    }
403}