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}