Skip to main content

ssh_key/
fingerprint.rs

1//! SSH public key fingerprints.
2
3mod randomart;
4
5use self::randomart::Randomart;
6use crate::{Error, HashAlg, Result, public};
7use core::{
8    fmt::{self, Display},
9    str::{self, FromStr},
10};
11use encoding::{
12    DigestWriter, Encode,
13    base64::{Base64Unpadded, Encoding},
14};
15use sha2::{Digest, Sha256, Sha512};
16
17#[cfg(feature = "alloc")]
18use alloc::string::{String, ToString};
19#[cfg(all(feature = "alloc", feature = "serde"))]
20use serde::{Deserialize, Serialize, de, ser};
21
22/// Fingerprint encoding error message.
23const FINGERPRINT_ERR_MSG: &str = "fingerprint encoding error";
24
25/// SSH public key fingerprints.
26///
27/// Fingerprints have an associated key fingerprint algorithm, i.e. a hash
28/// function which is used to compute the fingerprint.
29///
30/// # Parsing/serializing fingerprint strings
31///
32/// The [`FromStr`] and [`Display`] impls on [`Fingerprint`] can be used to
33/// parse and serialize fingerprints from the string format.
34///
35/// ### Example
36///
37/// ```text
38/// SHA256:Nh0Me49Zh9fDw/VYUfq43IJmI1T+XrjiYONPND8GzaM
39/// ```
40///
41/// # `serde` support
42///
43/// When the `serde` feature of this crate is enabled, this type receives impls
44/// of [`Deserialize`][`serde::Deserialize`] and [`Serialize`][`serde::Serialize`].
45#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
46#[non_exhaustive]
47pub enum Fingerprint {
48    /// Fingerprints computed using SHA-256.
49    Sha256([u8; HashAlg::Sha256.digest_size()]),
50
51    /// Fingerprints computed using SHA-512.
52    Sha512([u8; HashAlg::Sha512.digest_size()]),
53}
54
55impl Fingerprint {
56    /// Size of a SHA-512 hash encoded as Base64.
57    const SHA512_BASE64_SIZE: usize = 86;
58
59    /// Create a fingerprint of the given public key data using the provided
60    /// hash algorithm.
61    #[must_use]
62    #[allow(clippy::missing_panics_doc, reason = "should not panic")]
63    pub fn new(algorithm: HashAlg, public_key: &public::KeyData) -> Self {
64        match algorithm {
65            HashAlg::Sha256 => {
66                let mut digest = Sha256::new();
67                public_key
68                    .encode(&mut DigestWriter(&mut digest))
69                    .expect(FINGERPRINT_ERR_MSG);
70                Self::Sha256(digest.finalize().into())
71            }
72            HashAlg::Sha512 => {
73                let mut digest = Sha512::new();
74                public_key
75                    .encode(&mut DigestWriter(&mut digest))
76                    .expect(FINGERPRINT_ERR_MSG);
77                Self::Sha512(digest.finalize().into())
78            }
79        }
80    }
81
82    /// Get the hash algorithm used for this fingerprint.
83    #[must_use]
84    pub fn algorithm(self) -> HashAlg {
85        match self {
86            Self::Sha256(_) => HashAlg::Sha256,
87            Self::Sha512(_) => HashAlg::Sha512,
88        }
89    }
90
91    /// Get the name of the hash algorithm (upper case e.g. "SHA256").
92    #[must_use]
93    pub fn prefix(self) -> &'static str {
94        match self.algorithm() {
95            HashAlg::Sha256 => "SHA256",
96            HashAlg::Sha512 => "SHA512",
97        }
98    }
99
100    /// Get the bracketed hash algorithm footer for use in "randomart".
101    fn footer(self) -> &'static str {
102        match self.algorithm() {
103            HashAlg::Sha256 => "[SHA256]",
104            HashAlg::Sha512 => "[SHA512]",
105        }
106    }
107
108    /// Get the raw digest output for the fingerprint as bytes.
109    #[must_use]
110    pub fn as_bytes(&self) -> &[u8] {
111        match self {
112            Self::Sha256(bytes) => bytes.as_slice(),
113            Self::Sha512(bytes) => bytes.as_slice(),
114        }
115    }
116
117    /// Get the SHA-256 fingerprint, if this is one.
118    #[must_use]
119    pub fn sha256(self) -> Option<[u8; HashAlg::Sha256.digest_size()]> {
120        match self {
121            Self::Sha256(fingerprint) => Some(fingerprint),
122            _ => None,
123        }
124    }
125
126    /// Get the SHA-512 fingerprint, if this is one.
127    #[must_use]
128    pub fn sha512(self) -> Option<[u8; HashAlg::Sha512.digest_size()]> {
129        match self {
130            Self::Sha512(fingerprint) => Some(fingerprint),
131            _ => None,
132        }
133    }
134
135    /// Is this fingerprint SHA-256?
136    #[must_use]
137    pub fn is_sha256(self) -> bool {
138        matches!(self, Self::Sha256(_))
139    }
140
141    /// Is this fingerprint SHA-512?
142    #[must_use]
143    pub fn is_sha512(self) -> bool {
144        matches!(self, Self::Sha512(_))
145    }
146
147    /// Format "randomart" for this fingerprint using the provided formatter.
148    ///
149    /// # Errors
150    /// Propagates errors from [`fmt::Formatter`].
151    pub fn fmt_randomart(self, header: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        Randomart::new(header, self).fmt(f)
153    }
154
155    /// Render "randomart" hash visualization for this fingerprint as a string.
156    ///
157    /// ```text
158    /// +--[ED25519 256]--+
159    /// |o+oO==+ o..      |
160    /// |.o++Eo+o..       |
161    /// |. +.oO.o . .     |
162    /// | . o..B.. . .    |
163    /// |  ...+ .S. o     |
164    /// |  .o. . . . .    |
165    /// |  o..    o       |
166    /// |   B      .      |
167    /// |  .o*            |
168    /// +----[SHA256]-----+
169    /// ```
170    #[cfg(feature = "alloc")]
171    #[must_use]
172    pub fn to_randomart(self, header: &str) -> String {
173        Randomart::new(header, self).to_string()
174    }
175}
176
177impl AsRef<[u8]> for Fingerprint {
178    fn as_ref(&self) -> &[u8] {
179        self.as_bytes()
180    }
181}
182
183impl Display for Fingerprint {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        let prefix = self.prefix();
186
187        // Buffer size is the largest digest size of of any supported hash function
188        let mut buf = [0u8; Self::SHA512_BASE64_SIZE];
189        let base64 = Base64Unpadded::encode(self.as_bytes(), &mut buf).map_err(|_| fmt::Error)?;
190        write!(f, "{prefix}:{base64}")
191    }
192}
193
194impl FromStr for Fingerprint {
195    type Err = Error;
196
197    fn from_str(id: &str) -> Result<Self> {
198        let (alg_str, base64) = id.split_once(':').ok_or(Error::AlgorithmUnknown)?;
199
200        // Fingerprints use a special upper-case hash algorithm encoding.
201        let algorithm = match alg_str {
202            "SHA256" => HashAlg::Sha256,
203            "SHA512" => HashAlg::Sha512,
204            _ => return Err(Error::AlgorithmUnknown),
205        };
206
207        // Buffer size is the largest digest size of of any supported hash function
208        let mut buf = [0u8; HashAlg::Sha512.digest_size()];
209        let decoded_bytes = Base64Unpadded::decode(base64, &mut buf)?;
210
211        match algorithm {
212            HashAlg::Sha256 => Ok(Self::Sha256(decoded_bytes.try_into()?)),
213            HashAlg::Sha512 => Ok(Self::Sha512(decoded_bytes.try_into()?)),
214        }
215    }
216}
217
218#[cfg(all(feature = "alloc", feature = "serde"))]
219impl<'de> Deserialize<'de> for Fingerprint {
220    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
221    where
222        D: de::Deserializer<'de>,
223    {
224        let string = String::deserialize(deserializer)?;
225        string.parse().map_err(de::Error::custom)
226    }
227}
228
229#[cfg(all(feature = "alloc", feature = "serde"))]
230impl Serialize for Fingerprint {
231    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
232    where
233        S: ser::Serializer,
234    {
235        self.to_string().serialize(serializer)
236    }
237}