ecdsa.com

Key fingerprint · Go

How to compute a public key fingerprint in Go

In Go, the public key fingerprint falls out of the PEM structure directly: the body of a BEGIN PUBLIC KEY block already is SPKI DER, so sha256.Sum256 over block.Bytes is the entire computation — with an optional parse to guarantee the bytes are a real key.

Tested with Go 1.26 (standard library only).

fingerprint.go — SHA-256 over SPKI DER
package main
 
import (
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"os"
"strings"
)
 
func main() {
pemBytes, _ := os.ReadFile("pub.pem")
block, _ := pem.Decode(pemBytes) // "PUBLIC KEY" — the body is SPKI DER
 
// parse first: fingerprinting garbage should fail loudly
if _, err := x509.ParsePKIXPublicKey(block.Bytes); err != nil {
panic(err)
}
 
sum := sha256.Sum256(block.Bytes)
digest := hex.EncodeToString(sum[:])
 
var parts []string
for i := 0; i < len(digest); i += 2 {
parts = append(parts, digest[i:i+2])
}
fmt.Println(strings.Join(parts, ":"))
}
from an ecdsa.PublicKey value in memory
spki, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
if err != nil {
panic(err)
}
sum := sha256.Sum256(spki)

How it works

  1. For PEM input, block.Bytes is already the DER to hash — no marshal step needed.
  2. The ParsePKIXPublicKey call is a guard: it makes a corrupted or mislabeled file fail with a parse error instead of yielding a confident fingerprint of noise.
  3. For in-memory keys, MarshalPKIXPublicKey produces the identical SPKI bytes, so both paths agree on the digest.

Gotchas

  • Hash block.Bytes, never pemBytes — the PEM text differs across tools (line width, trailing newline) while the DER inside is canonical.
  • base64.StdEncoding.EncodeToString(sum[:]) gives the pin-sha256 form; trimming the = padding is conventional in some configs — decide, then be consistent.
  • If the file might be a certificate rather than a key, pem.Decode's block type will read CERTIFICATE — hash of that DER is the cert fingerprint, a different value. Check block.Type.

Related recipes