[dependencies]p256 = { version = "=0.14.0", features = ["pem"] }sha2 = "=0.11.1"base64 = "=0.23.1"x509-parser = "=0.18.1" use base64::Engine;use base64::engine::general_purpose::STANDARD;use p256::PublicKey;use p256::pkcs8::{DecodePublicKey, EncodePublicKey};use sha2::{Digest, Sha256}; fn main() -> Result<(), Box<dyn std::error::Error>> { // parse first: a damaged file fails here instead of yielding a fingerprint of noise let key = PublicKey::from_public_key_pem(&std::fs::read_to_string("pub.pem")?)?; let spki = key.to_public_key_der()?; // canonical SPKI DER, 91 bytes for P-256 let digest = Sha256::digest(spki.as_bytes()); let hex: Vec<String> = digest.iter().map(|b| format!("{b:02x}")).collect(); println!("{}", hex.join(":")); // the layout of openssl dgst -c println!("{}", STANDARD.encode(digest)); // pin-sha256 (RFC 7469) Ok(())} // The key inside a certificate: hash its SubjectPublicKeyInfo, not the whole certificatelet data = std::fs::read("cert.pem")?;let (_, pem) = x509_parser::pem::parse_x509_pem(&data)?;let cert = pem.parse_x509()?;let key_digest = Sha256::digest(cert.public_key().raw); // raw = the SPKI DER as it appears in the certificateprintln!("certificate key: {}", key_digest.iter().map(|b| format!("{b:02x}")).collect::<String>()); How it works
PublicKey::from_public_key_pemvalidates the key (a point on P-256) before anything is hashed.to_public_key_derre-encodes the canonical SPKI; hashing those 91 bytes gives the same digest asopenssl pkey -pubin -outform DER | openssl dgst -sha256, which the run compares.- The colon-separated hex is presentation only;
STANDARD.encode(digest)from thebase64crate gives thepin-sha256form. - In a certificate,
cert.public_key().rawis the SPKI exactly as encoded there, so it can be hashed without re-encoding the key.
Gotchas
- The certificate fingerprint (SHA-256 of the whole DER certificate,
54:7a:73:…for the example) is not the key fingerprint (3d:bb:28:…). Only the key fingerprint survives renewal with the same key. cert.public_key().rawis the SPKI as the CA encoded it. Certificates that carry explicit curve parameters instead of a named-curve OID hash differently from the same key re-encoded byto_public_key_der; re-encode when comparing keys from different sources.- Hashing
std::fs::read("pub.pem")hashes the PEM text — line endings included — and matches no other tool. - OpenSSH fingerprints (
SHA256:…fromssh-keygen -lf) hash the SSH wire encoding of the key and never equal the SPKI fingerprint.