Skip to main content

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    ///
129    /// # Errors
130    /// Returns [`Error::Encoding`] in the event of an encoding error.
131    pub fn from_openssh(public_key: &str) -> Result<Self> {
132        let encapsulation = SshFormat::decode(public_key.trim_end().as_bytes())?;
133        let mut reader = Base64Reader::new(encapsulation.base64_data)?;
134        let key_data = KeyData::decode(&mut reader)?;
135
136        // Verify that the algorithm in the Base64-encoded data matches the text
137        if encapsulation.algorithm_id != key_data.algorithm().as_str() {
138            return Err(Error::AlgorithmUnknown);
139        }
140
141        let public_key = Self {
142            key_data,
143            #[cfg(feature = "alloc")]
144            comment: encapsulation.comment.to_owned().into(),
145        };
146
147        Ok(reader.finish(public_key)?)
148    }
149
150    /// Parse a raw binary SSH public key.
151    ///
152    /// # Errors
153    /// Returns [`Error::Encoding`] in the event of an encoding error.
154    pub fn from_bytes(mut bytes: &[u8]) -> Result<Self> {
155        let reader = &mut bytes;
156        let key_data = KeyData::decode(reader)?;
157        Ok(reader.finish(key_data.into())?)
158    }
159
160    /// Encode OpenSSH-formatted public key.
161    ///
162    /// # Errors
163    /// Returns [`Error::Encoding`] in the event of an encoding error.
164    pub fn encode_openssh<'o>(&self, out: &'o mut [u8]) -> Result<&'o str> {
165        #[cfg(not(feature = "alloc"))]
166        let comment = "";
167        #[cfg(feature = "alloc")]
168        let comment = self.comment.as_str_lossy();
169
170        SshFormat::encode(self.algorithm().as_str(), &self.key_data, comment, out)
171    }
172
173    /// Encode an OpenSSH-formatted public key, allocating a [`String`] for the result.
174    ///
175    /// # Errors
176    /// Returns [`Error::Encoding`] in the event of an encoding error.
177    #[cfg(feature = "alloc")]
178    pub fn to_openssh(&self) -> Result<String> {
179        SshFormat::encode_string(
180            self.algorithm().as_str(),
181            &self.key_data,
182            self.comment.as_str_lossy(),
183        )
184    }
185
186    /// Serialize SSH public key as raw bytes.
187    ///
188    /// # Errors
189    /// Returns [`Error::Encoding`] in the event of an encoding error.
190    #[cfg(feature = "alloc")]
191    pub fn to_bytes(&self) -> Result<Vec<u8>> {
192        Ok(self.key_data.encode_vec()?)
193    }
194
195    /// Verify the [`SshSig`] signature is valid the given message using this public key.
196    ///
197    /// These signatures can be produced using `ssh-keygen -Y sign`. They're
198    /// encoded as PEM and begin with the following:
199    ///
200    /// ```text
201    /// -----BEGIN SSH SIGNATURE-----
202    /// ```
203    ///
204    /// See [PROTOCOL.sshsig] for more information.
205    ///
206    /// # Notes
207    ///
208    /// This method loads the  entire message has to be loaded into memory for verification.
209    /// If loading the entire message into memory is a problem consider computing a [Digest]
210    /// of the data first, and using [`PublicKey::verify_prehash`].
211    ///
212    /// # Usage
213    ///
214    /// See also: [`PrivateKey::sign`].
215    ///
216    #[cfg_attr(feature = "ed25519", doc = "```")]
217    #[cfg_attr(not(feature = "ed25519"), doc = "```ignore")]
218    /// # fn main() -> Result<(), ssh_key::Error> {
219    /// use ssh_key::{PublicKey, SshSig};
220    ///
221    /// // Message to be verified.
222    /// let message = b"testing";
223    ///
224    /// // Example domain/namespace used for the message.
225    /// let namespace = "example";
226    ///
227    /// // Public key which computed the signature.
228    /// let encoded_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com";
229    ///
230    /// // Example signature to be verified.
231    /// let signature_str = r#"
232    /// -----BEGIN SSH SIGNATURE-----
233    /// U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgsz6u836i33yqAQ3v3qNOJB9l8b
234    /// UppPQ+0UMn9cVKq2IAAAAHZXhhbXBsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQy
235    /// NTUxOQAAAEBPEav+tMGNnox4MuzM7rlHyVBajCn8B0kAyiOWwPKprNsG3i6X+voz/WCSik
236    /// /FowYwqhgCABUJSvRX3AERVBUP
237    /// -----END SSH SIGNATURE-----
238    /// "#;
239    ///
240    /// let public_key = encoded_public_key.parse::<PublicKey>()?;
241    /// let signature = signature_str.parse::<SshSig>()?;
242    /// public_key.verify(namespace, message, &signature)?;
243    /// # Ok(())
244    /// # }
245    /// ```
246    ///
247    /// [PROTOCOL.sshsig]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.sshsig?annotate=HEAD
248    /// [Digest]: https://docs.rs/digest/latest/digest/trait.Digest.html
249    ///
250    /// # Errors
251    /// - Returns [`Error::PublicKey`] if this key does not match the one in the signature.
252    /// - Returns [`Error::Namespace`] if the provided `namespace` does not match the signature.
253    /// - Returns [`Error::Signature`] in the event signature verification failed.
254    #[cfg(feature = "alloc")]
255    pub fn verify(&self, namespace: &str, msg: &[u8], signature: &SshSig) -> Result<()> {
256        self.verify_prehash(
257            namespace,
258            signature.hash_alg().digest(msg).as_slice(),
259            signature,
260        )
261    }
262
263    /// Verify the [`SshSig`] signature is valid the given message [`Digest`] using this public key.
264    ///
265    /// See [`PublicKey::verify`] for more information.
266    ///
267    /// # Errors
268    /// - Returns [`Error::PublicKey`] if this key does not match the one in the signature.
269    /// - Returns [`Error::Namespace`] if the provided `namespace` does not match the signature.
270    /// - Returns [`Error::Signature`] in the event signature verification failed.
271    #[cfg(feature = "alloc")]
272    pub fn verify_digest<D: AssociatedHashAlg + Digest>(
273        &self,
274        namespace: &str,
275        digest: D,
276        signature: &SshSig,
277    ) -> Result<()> {
278        if D::HASH_ALG != signature.hash_alg() {
279            return Err(Error::Crypto);
280        }
281
282        self.verify_prehash(namespace, digest.finalize().as_slice(), signature)
283    }
284
285    /// Verify the [`SshSig`] signature matches the given prehashed message digest using this
286    /// public key.
287    ///
288    /// See [`PublicKey::verify`] for more information.
289    ///
290    /// # Errors
291    /// - Returns [`Error::PublicKey`] if this key does not match the one in the signature.
292    /// - Returns [`Error::Namespace`] if the provided `namespace` does not match the signature.
293    /// - Returns [`Error::Signature`] in the event signature verification failed.
294    #[cfg(feature = "alloc")]
295    pub fn verify_prehash(
296        &self,
297        namespace: &str,
298        prehash: &[u8],
299        signature: &SshSig,
300    ) -> Result<()> {
301        if self.key_data() != signature.public_key() {
302            return Err(Error::PublicKey);
303        }
304
305        if namespace != signature.namespace() {
306            return Err(Error::Namespace);
307        }
308
309        signature.verify_prehash(prehash)
310    }
311
312    /// Read public key from an OpenSSH-formatted source.
313    ///
314    /// # Errors
315    /// - Returns [`Error::Io`] in the event of an I/O error.
316    /// - Returns [`Error::Encoding`] in the event of an encoding error.
317    #[cfg(feature = "std")]
318    pub fn read_openssh(reader: &mut impl Read) -> Result<Self> {
319        let input = io::read_to_string(reader)?;
320        Self::from_openssh(&input)
321    }
322
323    /// Read public key from an OpenSSH-formatted file.
324    ///
325    /// # Errors
326    /// - Returns [`Error::Io`] in the event of an I/O error.
327    /// - Returns [`Error::Encoding`] in the event of an encoding error.
328    #[cfg(feature = "std")]
329    pub fn read_openssh_file(path: impl AsRef<Path>) -> Result<Self> {
330        let mut file = File::open(path)?;
331        Self::read_openssh(&mut file)
332    }
333
334    /// Write public key as an OpenSSH-formatted file.
335    ///
336    /// # Errors
337    /// - Returns [`Error::Io`] in the event of an I/O error.
338    /// - Returns [`Error::Encoding`] in the event of an encoding error.
339    #[cfg(feature = "std")]
340    pub fn write_openssh(&self, writer: &mut impl Write) -> Result<()> {
341        let mut encoded = self.to_openssh()?;
342        encoded.push('\n'); // TODO(tarcieri): OS-specific line endings?
343
344        writer.write_all(encoded.as_bytes())?;
345        Ok(())
346    }
347
348    /// Write public key as an OpenSSH-formatted file.
349    ///
350    /// # Errors
351    /// - Returns [`Error::Io`] in the event of an I/O error.
352    /// - Returns [`Error::Encoding`] in the event of an encoding error.
353    #[cfg(feature = "std")]
354    pub fn write_openssh_file(&self, path: impl AsRef<Path>) -> Result<()> {
355        let mut file = File::create(path)?;
356        self.write_openssh(&mut file)
357    }
358
359    /// Get the digital signature [`Algorithm`] used by this key.
360    #[must_use]
361    pub fn algorithm(&self) -> Algorithm {
362        self.key_data.algorithm()
363    }
364
365    /// Comment on the key (e.g. email address).
366    #[cfg(feature = "alloc")]
367    #[must_use]
368    pub fn comment(&self) -> &Comment {
369        &self.comment
370    }
371
372    /// Public key data.
373    #[must_use]
374    pub fn key_data(&self) -> &KeyData {
375        &self.key_data
376    }
377
378    /// Compute key fingerprint.
379    ///
380    /// Use [`Default::default()`] to use the default hash function (SHA-256).
381    #[must_use]
382    pub fn fingerprint(&self, hash_alg: HashAlg) -> Fingerprint {
383        self.key_data.fingerprint(hash_alg)
384    }
385
386    /// Set the comment on the key.
387    #[cfg(feature = "alloc")]
388    pub fn set_comment(&mut self, comment: impl Into<Comment>) {
389        self.comment = comment.into();
390    }
391
392    /// Decode comment (e.g. email address).
393    ///
394    /// This is a stub implementation that ignores the comment.
395    #[cfg(not(feature = "alloc"))]
396    pub(crate) fn decode_comment(&mut self, reader: &mut impl Reader) -> Result<()> {
397        reader.drain_prefixed()?;
398        Ok(())
399    }
400
401    /// Decode comment (e.g. email address)
402    #[cfg(feature = "alloc")]
403    pub(crate) fn decode_comment(&mut self, reader: &mut impl Reader) -> Result<()> {
404        self.comment = Comment::decode(reader)?;
405        Ok(())
406    }
407}
408
409impl From<KeyData> for PublicKey {
410    fn from(key_data: KeyData) -> PublicKey {
411        PublicKey {
412            key_data,
413            #[cfg(feature = "alloc")]
414            comment: Comment::default(),
415        }
416    }
417}
418
419impl From<PublicKey> for KeyData {
420    fn from(public_key: PublicKey) -> KeyData {
421        public_key.key_data
422    }
423}
424
425impl From<&PublicKey> for KeyData {
426    fn from(public_key: &PublicKey) -> KeyData {
427        public_key.key_data.clone()
428    }
429}
430
431#[cfg(feature = "alloc")]
432impl From<DsaPublicKey> for PublicKey {
433    fn from(public_key: DsaPublicKey) -> PublicKey {
434        KeyData::from(public_key).into()
435    }
436}
437
438#[cfg(feature = "ecdsa")]
439impl From<EcdsaPublicKey> for PublicKey {
440    fn from(public_key: EcdsaPublicKey) -> PublicKey {
441        KeyData::from(public_key).into()
442    }
443}
444
445impl From<Ed25519PublicKey> for PublicKey {
446    fn from(public_key: Ed25519PublicKey) -> PublicKey {
447        KeyData::from(public_key).into()
448    }
449}
450
451#[cfg(feature = "alloc")]
452impl From<RsaPublicKey> for PublicKey {
453    fn from(public_key: RsaPublicKey) -> PublicKey {
454        KeyData::from(public_key).into()
455    }
456}
457
458#[cfg(feature = "ecdsa")]
459impl From<SkEcdsaSha2NistP256> for PublicKey {
460    fn from(public_key: SkEcdsaSha2NistP256) -> PublicKey {
461        KeyData::from(public_key).into()
462    }
463}
464
465impl From<SkEd25519> for PublicKey {
466    fn from(public_key: SkEd25519) -> PublicKey {
467        KeyData::from(public_key).into()
468    }
469}
470
471impl FromStr for PublicKey {
472    type Err = Error;
473
474    fn from_str(s: &str) -> Result<Self> {
475        Self::from_openssh(s)
476    }
477}
478
479#[cfg(feature = "alloc")]
480#[allow(clippy::to_string_trait_impl)]
481impl ToString for PublicKey {
482    fn to_string(&self) -> String {
483        self.to_openssh().expect("SSH public key encoding error")
484    }
485}
486
487#[cfg(all(feature = "alloc", feature = "serde"))]
488impl<'de> Deserialize<'de> for PublicKey {
489    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
490    where
491        D: de::Deserializer<'de>,
492    {
493        if deserializer.is_human_readable() {
494            let string = String::deserialize(deserializer)?;
495            Self::from_openssh(&string).map_err(de::Error::custom)
496        } else {
497            let bytes = Vec::<u8>::deserialize(deserializer)?;
498            Self::from_bytes(&bytes).map_err(de::Error::custom)
499        }
500    }
501}
502
503#[cfg(all(feature = "alloc", feature = "serde"))]
504impl Serialize for PublicKey {
505    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
506    where
507        S: ser::Serializer,
508    {
509        if serializer.is_human_readable() {
510            self.to_openssh()
511                .map_err(ser::Error::custom)?
512                .serialize(serializer)
513        } else {
514            self.to_bytes()
515                .map_err(ser::Error::custom)?
516                .serialize(serializer)
517        }
518    }
519}