use crate::core::{AsxError, ErrorCode, ErrorContext, Result};
use sha2::{Digest, Sha256};
use std::fmt;
const SHA256_HEX_LEN: usize = 64;
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CertFingerprint(String);
impl CertFingerprint {
#[cfg(feature = "crypto")]
#[cfg_attr(docsrs, doc(cfg(feature = "crypto")))]
pub fn from_cert_pem(cert_pem: &[u8]) -> Result<Self> {
let cert = openssl::x509::X509::from_pem(cert_pem).map_err(|err| {
AsxError::new(
ErrorCode::InvalidInput,
format!("not a PEM X.509 certificate: {err}"),
ErrorContext::new("cert_fingerprint_from_pem"),
)
})?;
let der = cert.to_der().map_err(|err| {
AsxError::new(
ErrorCode::InvalidInput,
format!("certificate could not be re-encoded as DER: {err}"),
ErrorContext::new("cert_fingerprint_from_pem"),
)
})?;
Ok(Self::from_cert_der(&der))
}
pub fn from_cert_der(cert_der: &[u8]) -> Self {
let digest = Sha256::digest(cert_der);
let mut out = String::with_capacity(SHA256_HEX_LEN);
for byte in digest {
use fmt::Write as _;
let _ = write!(out, "{byte:02x}");
}
Self(out)
}
pub fn parse(value: &str) -> Result<Self> {
let normalized: String = value
.chars()
.filter(char::is_ascii_hexdigit)
.map(|c| c.to_ascii_lowercase())
.collect();
if normalized.len() != SHA256_HEX_LEN {
return Err(AsxError::new(
ErrorCode::InvalidInput,
format!(
"a SHA-256 certificate fingerprint has {SHA256_HEX_LEN} hex digits, \
this one has {} — check for a truncated or SHA-1 value",
normalized.len()
),
ErrorContext::new("cert_fingerprint_parse"),
));
}
Ok(Self(normalized))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CertFingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Debug for CertFingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CertFingerprint({})", self.0)
}
}
impl std::str::FromStr for CertFingerprint {
type Err = AsxError;
fn from_str(s: &str) -> Result<Self> {
Self::parse(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
const HEX64: &str = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
#[test]
fn parse_normalizes_separators_and_case() {
let colons = "AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:\
AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89";
assert_eq!(
CertFingerprint::parse(colons).expect("parse"),
CertFingerprint::parse(HEX64).expect("parse")
);
}
#[test]
fn parse_rejects_a_truncated_value() {
let err = CertFingerprint::parse(&HEX64[..40]).expect_err("must reject");
assert_eq!(err.code, ErrorCode::InvalidInput);
assert!(
err.message.contains("64 hex digits"),
"the error must name the expected length: {}",
err.message
);
}
#[test]
fn parse_rejects_a_sha1_fingerprint() {
let sha1 = "AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01";
assert!(CertFingerprint::parse(sha1).is_err());
}
#[test]
fn parse_rejects_empty_and_non_hex() {
assert!(CertFingerprint::parse("").is_err());
assert!(CertFingerprint::parse("not-a-fingerprint").is_err());
}
#[test]
fn der_digest_matches_a_known_vector() {
assert_eq!(
CertFingerprint::from_cert_der(b"").as_str(),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn from_cert_pem_rejects_non_certificate_input() {
let err = CertFingerprint::from_cert_pem(b"-----BEGIN CERTIFICATE-----\nnope\n")
.expect_err("must reject");
assert_eq!(err.code, ErrorCode::InvalidInput);
}
}