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