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, ":"))}spki, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)if err != nil { panic(err)}sum := sha256.Sum256(spki)How it works
- For PEM input,
block.Bytesis already the DER to hash — no marshal step needed. - The
ParsePKIXPublicKeycall is a guard: it makes a corrupted or mislabeled file fail with a parse error instead of yielding a confident fingerprint of noise. - For in-memory keys,
MarshalPKIXPublicKeyproduces the identical SPKI bytes, so both paths agree on the digest.
Gotchas
- Hash
block.Bytes, neverpemBytes— 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 readCERTIFICATE— hash of that DER is the cert fingerprint, a different value. Checkblock.Type.