require "openssl" public_key = OpenSSL::PKey.read(File.read("pub.pem")) # parsing first: a non-key fails heredigest = OpenSSL::Digest::SHA256.digest(public_key.public_to_der) # SPKI DER, 91 bytes for P-256 puts digest.unpack1("H*").scan(/../).join(":") # the layout of openssl dgst -cputs [digest].pack("m0") # pin-sha256 (RFC 7469) # The key inside a certificate: hash its SPKI, not the certificatecert = OpenSSL::X509::Certificate.new(File.read("cert.pem"))puts "certificate key: #{OpenSSL::Digest::SHA256.hexdigest(cert.public_key.public_to_der)}"puts "certificate itself: #{OpenSSL::Digest::SHA256.hexdigest(cert.to_der)}" How it works
OpenSSL::PKey.readparses the key first, so a damaged file raisesCould not parse PKeyinstead of yielding a fingerprint of noise.public_to_derre-encodes the public half as canonical SPKI DER — the same 91 bytes for a P-256 key whether it came from a PEM file, a private key or a certificate.unpack1("H*")withscan(/../).join(":")gives theopenssl dgst -clayout;pack("m0")the strict base64pin-sha256form.- For a certificate,
cert.public_key.public_to_derhashes the key;cert.to_derwould hash the whole certificate. The run prints both and compares the key digest with OpenSSL.
Gotchas
- Called on a private key,
to_derreturns the private key encoding, not SPKI — always usepublic_to_derfor fingerprints, which works on private and public key objects alike. - The certificate fingerprint (
54:7a:73:…for the example) changes on every renewal; the key fingerprint (3d:bb:28:…) stays the same as long as the key does — pin that one. OpenSSL::Digest::SHA256.hexdigest(File.read("pub.pem"))hashes PEM text and matches no other tool.- OpenSSH fingerprints (
SHA256:…fromssh-keygen -lf) hash the SSH encoding of the key and never equal the SPKI value.