ssh_key/
fingerprint.rs

1//! SSH public key fingerprints.
2
3mod randomart;
4
5use self::randomart::Randomart;
6use crate::{public, Error, HashAlg, Result};
7use core::{
8    fmt::{self, Display},
9    str::{self, FromStr},
10};
11use encoding::{
12    base64::{Base64Unpadded, Encoding},
13    Encode,
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::{de, ser, Deserialize, Serialize};
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)]
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.encode(&mut digest).expect(FINGERPRINT_ERR_MSG);
67                Self::Sha256(digest.finalize().into())
68            }
69            HashAlg::Sha512 => {
70                let mut digest = Sha512::new();
71                public_key.encode(&mut digest).expect(FINGERPRINT_ERR_MSG);
72                Self::Sha512(digest.finalize().into())
73            }
74        }
75    }
76
77    /// Get the hash algorithm used for this fingerprint.
78    pub fn algorithm(self) -> HashAlg {
79        match self {
80            Self::Sha256(_) => HashAlg::Sha256,
81            Self::Sha512(_) => HashAlg::Sha512,
82        }
83    }
84
85    /// Get the name of the hash algorithm (upper case e.g. "SHA256").
86    pub fn prefix(self) -> &'static str {
87        match self.algorithm() {
88            HashAlg::Sha256 => "SHA256",
89            HashAlg::Sha512 => "SHA512",
90        }
91    }
92
93    /// Get the bracketed hash algorithm footer for use in "randomart".
94    fn footer(self) -> &'static str {
95        match self.algorithm() {
96            HashAlg::Sha256 => "[SHA256]",
97            HashAlg::Sha512 => "[SHA512]",
98        }
99    }
100
101    /// Get the raw digest output for the fingerprint as bytes.
102    pub fn as_bytes(&self) -> &[u8] {
103        match self {
104            Self::Sha256(bytes) => bytes.as_slice(),
105            Self::Sha512(bytes) => bytes.as_slice(),
106        }
107    }
108
109    /// Get the SHA-256 fingerprint, if this is one.
110    pub fn sha256(self) -> Option<[u8; HashAlg::Sha256.digest_size()]> {
111        match self {
112            Self::Sha256(fingerprint) => Some(fingerprint),
113            _ => None,
114        }
115    }
116
117    /// Get the SHA-512 fingerprint, if this is one.
118    pub fn sha512(self) -> Option<[u8; HashAlg::Sha512.digest_size()]> {
119        match self {
120            Self::Sha512(fingerprint) => Some(fingerprint),
121            _ => None,
122        }
123    }
124
125    /// Is this fingerprint SHA-256?
126    pub fn is_sha256(self) -> bool {
127        matches!(self, Self::Sha256(_))
128    }
129
130    /// Is this fingerprint SHA-512?
131    pub fn is_sha512(self) -> bool {
132        matches!(self, Self::Sha512(_))
133    }
134
135    /// Format "randomart" for this fingerprint using the provided formatter.
136    pub fn fmt_randomart(self, header: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        Randomart::new(header, self).fmt(f)
138    }
139
140    /// Render "randomart" hash visualization for this fingerprint as a string.
141    ///
142    /// ```text
143    /// +--[ED25519 256]--+
144    /// |o+oO==+ o..      |
145    /// |.o++Eo+o..       |
146    /// |. +.oO.o . .     |
147    /// | . o..B.. . .    |
148    /// |  ...+ .S. o     |
149    /// |  .o. . . . .    |
150    /// |  o..    o       |
151    /// |   B      .      |
152    /// |  .o*            |
153    /// +----[SHA256]-----+
154    /// ```
155    #[cfg(feature = "alloc")]
156    pub fn to_randomart(self, header: &str) -> String {
157        Randomart::new(header, self).to_string()
158    }
159}
160
161impl AsRef<[u8]> for Fingerprint {
162    fn as_ref(&self) -> &[u8] {
163        self.as_bytes()
164    }
165}
166
167impl Display for Fingerprint {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        let prefix = self.prefix();
170
171        // Buffer size is the largest digest size of of any supported hash function
172        let mut buf = [0u8; Self::SHA512_BASE64_SIZE];
173        let base64 = Base64Unpadded::encode(self.as_bytes(), &mut buf).map_err(|_| fmt::Error)?;
174        write!(f, "{prefix}:{base64}")
175    }
176}
177
178impl FromStr for Fingerprint {
179    type Err = Error;
180
181    fn from_str(id: &str) -> Result<Self> {
182        let (alg_str, base64) = id.split_once(':').ok_or(Error::AlgorithmUnknown)?;
183
184        // Fingerprints use a special upper-case hash algorithm encoding.
185        let algorithm = match alg_str {
186            "SHA256" => HashAlg::Sha256,
187            "SHA512" => HashAlg::Sha512,
188            _ => return Err(Error::AlgorithmUnknown),
189        };
190
191        // Buffer size is the largest digest size of of any supported hash function
192        let mut buf = [0u8; HashAlg::Sha512.digest_size()];
193        let decoded_bytes = Base64Unpadded::decode(base64, &mut buf)?;
194
195        match algorithm {
196            HashAlg::Sha256 => Ok(Self::Sha256(decoded_bytes.try_into()?)),
197            HashAlg::Sha512 => Ok(Self::Sha512(decoded_bytes.try_into()?)),
198        }
199    }
200}
201
202#[cfg(all(feature = "alloc", feature = "serde"))]
203impl<'de> Deserialize<'de> for Fingerprint {
204    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
205    where
206        D: de::Deserializer<'de>,
207    {
208        let string = String::deserialize(deserializer)?;
209        string.parse().map_err(de::Error::custom)
210    }
211}
212
213#[cfg(all(feature = "alloc", feature = "serde"))]
214impl Serialize for Fingerprint {
215    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
216    where
217        S: ser::Serializer,
218    {
219        self.to_string().serialize(serializer)
220    }
221}