TLS Certificate Fingerprints

Posted by Alex D'Andrea on 31 January 2024

I need to manage TLS certificates programmatically. Certificates are the foundation of the encrypted transport in the world wide web, so every browser supports TLS. The certificates can be quite lengthy, depending on the key size, and in their raw, but human readable form, look a bit like this:

$ cat - > alex.dandrea.io.pem
-----BEGIN CERTIFICATE-----
MIIF3TCCBMWgAwIBAgIQBSvTgT1PdY1aUR2hKpnwMDANBgkqhkiG9w0BAQsFADA8
MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRwwGgYDVQQDExNBbWF6b24g
[... omit 41 lines ...]
GIC4lTEmbaeXeH5zl+TwOENSS4XA/QK+3gMjLqsEpTCtD5J+t1vcjFwXg7UE1jXc
RSF3jJGNSvJp2i3/xNWZXxU=
-----END CERTIFICATE-----

(this is the certificate of this website as of January 2024.)

So in order to manage a lot of those, I’d like to have a shorted form, a digest or a hash. Firefox shows this for this certificate:

Firefox’s TLS certificate overview

I could just build a hash from the certificate contents above, but that does not match Firefox’s value, and I like to avoid reinventing the wheel. It would be nice to be able to extract exactly these fingerprints from the PEM certificates. Today I learned the fingerprints are not stored in the certificate, but rather calculated from it, and the calculation is a message digest over the DER form with SHA256 (or SHA1); and this is the actually interesting part: the DER form is just the binary representation of the PEM, omitting the BEGIN (------BEGIN ...) and END (-----END ...) markers and removing the newlines.

To generate these fingerprints on the commandline, do this:

# Convert PEM file into DER form
$ openssl x509 -in alex.dandrea.io.pem -out alex.dandrea.io.cer -outform DER

$ sha256sum alex.dandrea.io.cer
ab2a6dba85af4e6af6e1899deb80671f6f9c43bb202d4a94c6021ed2fd4b3d2d  alex.dandrea.io.cer

$ sha1sum alex.dandrea.io.cer
e199a03b1f7f1a6318bb00bd3830a998a2a65273  alex.dandrea.io.cer

Nice, but how can this be done in nodejs, ie. with node-forge? It is not so well documented, but you can get there with the following Typescript snippet:

import { pki, asn1, sha1, sha256 } from "node-forge";

const cert = pki.certificateFromPem("..." /* the PEM file data */);

// Convert pki.Certificate structure to asn.Asn1
const asn1Cert = pki.certificateToAsn1(cert);

// Convert asn1 to DER format, and wrap in buffer
const der = Buffer.from(asn1.toDer(asn1Cert).getBytes(), "binary");

console.log(
  "sha256 =",
  sha256.create().update(der.toString("binary")).digest().toHex(),
);
console.log(
  "sha1 =",
  sha1.create().update(der.toString("binary")).digest().toHex(),
);

Running this with the certificate data from above outputs:

$ yarn ts-node src/tls-fingerprint.ts
sha256 = ab2a6dba85af4e6af6e1899deb80671f6f9c43bb202d4a94c6021ed2fd4b3d2d
sha1 = e199a03b1f7f1a6318bb00bd3830a998a2a65273

\o/