ssh_key/
fingerprint.rs

1//! SSH public key fingerprints.
2
3use crate::{encode::Encode, public, Error, HashAlg, Result};
4use base64ct::{Base64Unpadded, Encoding};
5use core::{
6    fmt::{self, Display},
7    str::{self, FromStr},
8};
9use sha2::{Digest, Sha256, Sha512};
10
11/// Fingerprint encoding error message.
12const FINGERPRINT_ERR_MSG: &str = "fingerprint encoding error";
13
14#[cfg(all(feature = "alloc", feature = "serde"))]
15use {
16    alloc::string::{String, ToString},
17    serde::{de, ser, Deserialize, Serialize},
18};
19
20/// SSH public key fingerprints.
21///
22/// Fingerprints have an associated key fingerprint algorithm, i.e. a hash
23/// function which is used to compute the fingerprint.
24///
25/// # Parsing/serializing fingerprint strings
26///
27/// The [`FromStr`] and [`Display`] impls on [`Fingerprint`] can be used to
28/// parse and serialize fingerprints from the string format.
29///
30/// ### Example
31///
32/// ```text
33/// SHA256:Nh0Me49Zh9fDw/VYUfq43IJmI1T+XrjiYONPND8GzaM
34/// ```
35///
36/// # `serde` support
37///
38/// When the `serde` feature of this crate is enabled, this type receives impls
39/// of [`Deserialize`][`serde::Deserialize`] and [`Serialize`][`serde::Serialize`].
40#[cfg_attr(docsrs, doc(cfg(feature = "fingerprint")))]
41#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
42#[non_exhaustive]
43pub enum Fingerprint {
44    /// Fingerprints computed using SHA-256.
45    Sha256([u8; HashAlg::Sha256.digest_size()]),
46
47    /// Fingerprints computed using SHA-512.
48    Sha512([u8; HashAlg::Sha512.digest_size()]),
49}
50
51impl Fingerprint {
52    /// Size of a SHA-512 hash encoded as Base64.
53    const SHA512_BASE64_SIZE: usize = 86;
54
55    /// Create a fingerprint of the given public key data using the provided
56    /// hash algorithm.
57    pub fn new(algorithm: HashAlg, public_key: &public::KeyData) -> Self {
58        match algorithm {
59            HashAlg::Sha256 => {
60                let mut digest = Sha256::new();
61                public_key.encode(&mut digest).expect(FINGERPRINT_ERR_MSG);
62                Self::Sha256(digest.finalize().into())
63            }
64            HashAlg::Sha512 => {
65                let mut digest = Sha512::new();
66                public_key.encode(&mut digest).expect(FINGERPRINT_ERR_MSG);
67                Self::Sha512(digest.finalize().into())
68            }
69        }
70    }
71
72    /// Get the hash algorithm used for this fingerprint.
73    pub fn algorithm(self) -> HashAlg {
74        match self {
75            Self::Sha256(_) => HashAlg::Sha256,
76            Self::Sha512(_) => HashAlg::Sha512,
77        }
78    }
79
80    /// Get the raw digest output for the fingerprint as bytes.
81    pub fn as_bytes(&self) -> &[u8] {
82        match self {
83            Self::Sha256(bytes) => bytes.as_slice(),
84            Self::Sha512(bytes) => bytes.as_slice(),
85        }
86    }
87
88    /// Get the SHA-256 fingerprint, if this is one.
89    pub fn sha256(self) -> Option<[u8; HashAlg::Sha256.digest_size()]> {
90        match self {
91            Self::Sha256(fingerprint) => Some(fingerprint),
92            _ => None,
93        }
94    }
95
96    /// Get the SHA-512 fingerprint, if this is one.
97    pub fn sha512(self) -> Option<[u8; HashAlg::Sha512.digest_size()]> {
98        match self {
99            Self::Sha512(fingerprint) => Some(fingerprint),
100            _ => None,
101        }
102    }
103
104    /// Is this fingerprint SHA-256?
105    pub fn is_sha256(self) -> bool {
106        matches!(self, Self::Sha256(_))
107    }
108
109    /// Is this fingerprint SHA-512?
110    pub fn is_sha512(self) -> bool {
111        matches!(self, Self::Sha512(_))
112    }
113}
114
115impl AsRef<[u8]> for Fingerprint {
116    fn as_ref(&self) -> &[u8] {
117        self.as_bytes()
118    }
119}
120
121impl Display for Fingerprint {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        // Buffer size is the largest digest size of of any supported hash function
124        let mut buf = [0u8; Self::SHA512_BASE64_SIZE];
125        let base64 = Base64Unpadded::encode(self.as_bytes(), &mut buf).map_err(|_| fmt::Error)?;
126        write!(f, "{}:{}", self.algorithm(), base64)
127    }
128}
129
130impl FromStr for Fingerprint {
131    type Err = Error;
132
133    fn from_str(id: &str) -> Result<Self> {
134        let (algorithm, base64) = id.split_once(':').ok_or(Error::Algorithm)?;
135
136        // Buffer size is the largest digest size of of any supported hash function
137        let mut buf = [0u8; HashAlg::Sha512.digest_size()];
138        let decoded_bytes = Base64Unpadded::decode(base64, &mut buf)?;
139
140        match algorithm.parse()? {
141            HashAlg::Sha256 => Ok(Self::Sha256(decoded_bytes.try_into()?)),
142            HashAlg::Sha512 => Ok(Self::Sha512(decoded_bytes.try_into()?)),
143        }
144    }
145}
146
147#[cfg(all(feature = "alloc", feature = "serde"))]
148#[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "serde"))))]
149impl<'de> Deserialize<'de> for Fingerprint {
150    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
151    where
152        D: de::Deserializer<'de>,
153    {
154        let string = String::deserialize(deserializer)?;
155        string.parse().map_err(de::Error::custom)
156    }
157}
158
159#[cfg(all(feature = "alloc", feature = "serde"))]
160#[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "serde"))))]
161impl Serialize for Fingerprint {
162    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
163    where
164        S: ser::Serializer,
165    {
166        self.to_string().serialize(serializer)
167    }
168}