Repository: smallstep/crypto
Pull request #79 (2022-10-01 00:12:08)

Fingerprints
### Description

This PR extends the support for X.509 fingerprints to support all the options used by `step`. It also adds support for SSH fingerprints.

These functions will allow migrating more crypto-related code from `step` to `go.step.sm/crypto`


Patch:
diff --git a/internal/emoji/emoji.go b/internal/emoji/emoji.go
new file mode 100644
index 0000000..7235cff
--- /dev/null
+++ b/internal/emoji/emoji.go
@@ -0,0 +1,274 @@
+package emoji
+
+import "strings"
+
+func Emoji(input []byte) string {
+	var b strings.Builder
+	for _, r := range input {
+		b.WriteString(emojiCodeMap[r])
+	}
+	return b.String()
+}
+
+// emojiCodeMap is a mapping from byte to emoji.
+//
+// The mapping is based on draft+2 of https://github.com/emojisum/emojisum.
+// (see: https://github.com/emojisum/emojisum/releases/tag/draft%2B2)
+var emojiCodeMap = []string{
+	"\U0001f44d",                   // 👍 :+1:
+	"\U0001f3b1",                   // 🎱 :8ball:
+	"\u2708\ufe0f",                 // ✈️ :airplane:
+	"\U0001f47d",                   // 👽 :alien:
+	"\u2693",                       // ⚓ :anchor:
+	"\U0001f47c",                   // 👼 :angel:
+	"\U0001f620",                   // 😠 :angry:
+	"\U0001f41c",                   // 🐜 :ant:
+	"\U0001f34e",                   // 🍎 :apple:
+	"\U0001f3a8",                   // 🎨 :art:
+	"\U0001f476",                   // 👶 :baby:
+	"\U0001f37c",                   // 🍼 :baby_bottle:
+	"\U0001f519",                   // 🔙 :back:
+	"\U0001f38d",                   // 🎍 :bamboo:
+	"\U0001f34c",                   // 🍌 :banana:
+	"\U0001f488",                   // 💈 :barber:
+	"\U0001f6c1",                   // 🛁 :bathtub:
+	"\U0001f37a",                   // 🍺 :beer:
+	"\U0001f514",                   // 🔔 :bell:
+	"\U0001f6b4\u200d\u2642\ufe0f", // 🚴‍♂️ :bicyclist:
+	"\U0001f426",                   // 🐦 :bird:
+	"\U0001f382",                   // 🎂 :birthday:
+	"\U0001f33c",                   // 🌼 :blossom:
+	"\U0001f699",                   // 🚙 :blue_car:
+	"\U0001f417",                   // 🐗 :boar:
+	"\U0001f4a3",                   // 💣 :bomb:
+	"\U0001f4a5",                   // 💥 :boom:
+	"\U0001f647\u200d\u2642\ufe0f", // 🙇‍♂️ :bow:
+	"\U0001f466",                   // 👦 :boy:
+	"\U0001f494",                   // 💔 :broken_heart:
+	"\U0001f4a1",                   // 💡 :bulb:
+	"\U0001f68c",                   // 🚌 :bus:
+	"\U0001f335",                   // 🌵 :cactus:
+	"\U0001f4c6",                   // 📆 :calendar:
+	"\U0001f4f7",                   // 📷 :camera:
+	"\U0001f36c",                   // 🍬 :candy:
+	"\U0001f431",                   // 🐱 :cat:
+	"\U0001f352",                   // 🍒 :cherries:
+	"\U0001f6b8",                   // 🚸 :children_crossing:
+	"\U0001f36b",                   // 🍫 :chocolate_bar:
+	"\U0001f44f",                   // 👏 :clap:
+	"\u2601\ufe0f",                 // ☁️ :cloud:
+	"\u2663\ufe0f",                 // ♣️ :clubs:
+	"\U0001f1e8\U0001f1f3",         // 🇨🇳 :cn:
+	"\u2615",                       // ☕ :coffee:
+	"\U0001f6a7",                   // 🚧 :construction:
+	"\U0001f36a",                   // 🍪 :cookie:
+	"\u00a9\ufe0f",                 // ©️ :copyright:
+	"\U0001f33d",                   // 🌽 :corn:
+	"\U0001f42e",                   // 🐮 :cow:
+	"\U0001f319",                   // 🌙 :crescent_moon:
+	"\U0001f451",                   // 👑 :crown:
+	"\U0001f622",                   // 😢 :cry:
+	"\U0001f52e",                   // 🔮 :crystal_ball:
+	"\u27b0",                       // ➰ :curly_loop:
+	"\U0001f46f\u200d\u2640\ufe0f", // 👯‍♀️ :dancers:
+	"\U0001f4a8",                   // 💨 :dash:
+	"\U0001f1e9\U0001f1ea",         // 🇩🇪 :de:
+	"\u2666\ufe0f",                 // ♦️ :diamonds:
+	"\U0001f436",                   // 🐶 :dog:
+	"\U0001f369",                   // 🍩 :doughnut:
+	"\U0001f409",                   // 🐉 :dragon:
+	"\U0001f4c0",                   // 📀 :dvd:
+	"\U0001f442",                   // 👂 :ear:
+	"\U0001f346",                   // 🍆 :eggplant:
+	"\U0001f418",                   // 🐘 :elephant:
+	"\U0001f51a",                   // 🔚 :end:
+	"\u2709",                       // ✉ :envelope:
+	"\U0001f1ea\U0001f1f8",         // 🇪🇸 :es:
+	"\U0001f440",                   // 👀 :eyes:
+	"\U0001f44a",                   // 👊 :facepunch:
+	"\U0001f468\u200d\U0001f469\u200d\U0001f466", // 👨‍👩‍👦 :family:
+	"\U0001f3a1",                         // 🎡 :ferris_wheel:
+	"\U0001f630",                         // 😰 :cold_sweat:
+	"\U0001f525",                         // 🔥 :fire:
+	"\U0001f386",                         // 🎆 :fireworks:
+	"\U0001f4be",                         // 💾 :floppy_disk:
+	"\U0001f3c8",                         // 🏈 :football:
+	"\U0001f374",                         // 🍴 :fork_and_knife:
+	"\U0001f340",                         // 🍀 :four_leaf_clover:
+	"\U0001f1eb\U0001f1f7",               // 🇫🇷 :fr:
+	"\U0001f35f",                         // 🍟 :fries:
+	"\U0001f95c",                         // 🥜 :peanuts:
+	"\U0001f595",                         // 🖕 :fu:
+	"\U0001f315",                         // 🌕 :full_moon:
+	"\U0001f3b2",                         // 🎲 :game_die:
+	"\U0001f1ea\U0001f1fa",               // 🇪🇺 :eu:
+	"\U0001f48e",                         // 💎 :gem:
+	"\U0001f467",                         // 👧 :girl:
+	"\U0001f410",                         // 🐐 :goat:
+	"\U0001f62c",                         // 😬 :grimacing:
+	"\U0001f601",                         // 😁 :grin:
+	"\U0001f482\u200d\u2642\ufe0f",       // 💂‍♂️ :guardsman:
+	"\U0001f3b8",                         // 🎸 :guitar:
+	"\U0001f52b",                         // 🔫 :gun:
+	"\U0001f354",                         // 🍔 :hamburger:
+	"\U0001f528",                         // 🔨 :hammer:
+	"\U0001f439",                         // 🐹 :hamster:
+	"\U0001f649",                         // 🙉 :hear_no_evil:
+	"\u2764\ufe0f",                       // ❤️ :heart:
+	"\U0001f63b",                         // 😻 :heart_eyes_cat:
+	"\u2763\ufe0f",                       // ❣️ :heavy_heart_exclamation:
+	"\u2714\ufe0f",                       // ✔️ :heavy_check_mark:
+	"\U0001f5ff",                         // 🗿 :moyai:
+	"\U0001f3ee",                         // 🏮 :izakaya_lantern:
+	"\U0001f681",                         // 🚁 :helicopter:
+	"\U0001f52a",                         // 🔪 :hocho:
+	"\U0001f41d",                         // 🐝 :honeybee:
+	"\U0001f434",                         // 🐴 :horse:
+	"\U0001f3c7",                         // 🏇 :horse_racing:
+	"\u231b",                             // ⌛ :hourglass:
+	"\U0001f3e0",                         // 🏠 :house:
+	"\U0001f575\ufe0f\u200d\u2640\ufe0f", // 🕵️‍♀️ :female_detective:
+	"\U0001f366",                         // 🍦 :icecream:
+	"\U0001f47f",                         // 👿 :imp:
+	"\U0001f1ee\U0001f1f9",               // 🇮🇹 :it:
+	"\U0001f383",                         // 🎃 :jack_o_lantern:
+	"\U0001f47a",                         // 👺 :japanese_goblin:
+	"\U0001f1ef\U0001f1f5",               // 🇯🇵 :jp:
+	"\U0001f511",                         // 🔑 :key:
+	"\U0001f48b",                         // 💋 :kiss:
+	"\U0001f63d",                         // 😽 :kissing_cat:
+	"\U0001f428",                         // 🐨 :koala:
+	"\U0001f1f0\U0001f1f7",               // 🇰🇷 :kr:
+	"\U0001f34b",                         // 🍋 :lemon:
+	"\U0001f484",                         // 💄 :lipstick:
+	"\U0001f512",                         // 🔒 :lock:
+	"\U0001f36d",                         // 🍭 :lollipop:
+	"\U0001f468",                         // 👨 :man:
+	"\U0001f341",                         // 🍁 :maple_leaf:
+	"\U0001f637",                         // 😷 :mask:
+	"\U0001f918",                         // 🤘 :metal:
+	"\U0001f52c",                         // 🔬 :microscope:
+	"\U0001f4b0",                         // 💰 :moneybag:
+	"\U0001f412",                         // 🐒 :monkey:
+	"\U0001f5fb",                         // 🗻 :mount_fuji:
+	"\U0001f4aa",                         // 💪 :muscle:
+	"\U0001f344",                         // 🍄 :mushroom:
+	"\U0001f3b9",                         // 🎹 :musical_keyboard:
+	"\U0001f3bc",                         // 🎼 :musical_score:
+	"\U0001f485",                         // 💅 :nail_care:
+	"\U0001f311",                         // 🌑 :new_moon:
+	"\u26d4",                             // ⛔ :no_entry:
+	"\U0001f443",                         // 👃 :nose:
+	"\U0001f39b\ufe0f",                   // 🎛️ :control_knobs:
+	"\U0001f529",                         // 🔩 :nut_and_bolt:
+	"\u2b55",                             // ⭕ :o:
+	"\U0001f30a",                         // 🌊 :ocean:
+	"\U0001f44c",                         // 👌 :ok_hand:
+	"\U0001f51b",                         // 🔛 :on:
+	"\U0001f4e6",                         // 📦 :package:
+	"\U0001f334",                         // 🌴 :palm_tree:
+	"\U0001f43c",                         // 🐼 :panda_face:
+	"\U0001f4ce",                         // 📎 :paperclip:
+	"\u26c5",                             // ⛅ :partly_sunny:
+	"\U0001f6c2",                         // 🛂 :passport_control:
+	"\U0001f43e",                         // 🐾 :paw_prints:
+	"\U0001f351",                         // 🍑 :peach:
+	"\U0001f427",                         // 🐧 :penguin:
+	"\u260e\ufe0f",                       // ☎️ :phone:
+	"\U0001f437",                         // 🐷 :pig:
+	"\U0001f48a",                         // 💊 :pill:
+	"\U0001f34d",                         // 🍍 :pineapple:
+	"\U0001f355",                         // 🍕 :pizza:
+	"\U0001f448",                         // 👈 :point_left:
+	"\U0001f449",                         // 👉 :point_right:
+	"\U0001f4a9",                         // 💩 :poop:
+	"\U0001f357",                         // 🍗 :poultry_leg:
+	"\U0001f64f",                         // 🙏 :pray:
+	"\U0001f478",                         // 👸 :princess:
+	"\U0001f45b",                         // 👛 :purse:
+	"\U0001f4cc",                         // 📌 :pushpin:
+	"\U0001f430",                         // 🐰 :rabbit:
+	"\U0001f308",                         // 🌈 :rainbow:
+	"\u270b",                             // ✋ :raised_hand:
+	"\u267b\ufe0f",                       // ♻️ :recycle:
+	"\U0001f697",                         // 🚗 :red_car:
+	"\u00ae\ufe0f",                       // ®️ :registered:
+	"\U0001f380",                         // 🎀 :ribbon:
+	"\U0001f35a",                         // 🍚 :rice:
+	"\U0001f680",                         // 🚀 :rocket:
+	"\U0001f3a2",                         // 🎢 :roller_coaster:
+	"\U0001f413",                         // 🐓 :rooster:
+	"\U0001f1f7\U0001f1fa",               // 🇷🇺 :ru:
+	"\u26f5",                             // ⛵ :sailboat:
+	"\U0001f385",                         // 🎅 :santa:
+	"\U0001f6f0\ufe0f",                   // 🛰️ :satellite:
+	"\U0001f606",                         // 😆 :satisfied:
+	"\U0001f3b7",                         // 🎷 :saxophone:
+	"\u2702\ufe0f",                       // ✂️ :scissors:
+	"\U0001f648",                         // 🙈 :see_no_evil:
+	"\U0001f411",                         // 🐑 :sheep:
+	"\U0001f41a",                         // 🐚 :shell:
+	"\U0001f45e",                         // 👞 :shoe:
+	"\U0001f3bf",                         // 🎿 :ski:
+	"\U0001f480",                         // 💀 :skull:
+	"\U0001f62a",                         // 😪 :sleepy:
+	"\U0001f604",                         // 😄 :smile:
+	"\U0001f63a",                         // 😺 :smiley_cat:
+	"\U0001f60f",                         // 😏 :smirk:
+	"\U0001f6ac",                         // 🚬 :smoking:
+	"\U0001f40c",                         // 🐌 :snail:
+	"\U0001f40d",                         // 🐍 :snake:
+	"\u2744\ufe0f",                       // ❄️ :snowflake:
+	"\u26bd",                             // ⚽ :soccer:
+	"\U0001f51c",                         // 🔜 :soon:
+	"\U0001f47e",                         // 👾 :space_invader:
+	"\u2660\ufe0f",                       // ♠️ :spades:
+	"\U0001f64a",                         // 🙊 :speak_no_evil:
+	"\u2b50",                             // ⭐ :star:
+	"\u26f2",                             // ⛲ :fountain:
+	"\U0001f5fd",                         // 🗽 :statue_of_liberty:
+	"\U0001f682",                         // 🚂 :steam_locomotive:
+	"\U0001f33b",                         // 🌻 :sunflower:
+	"\U0001f60e",                         // 😎 :sunglasses:
+	"\u2600\ufe0f",                       // ☀️ :sunny:
+	"\U0001f305",                         // 🌅 :sunrise:
+	"\U0001f3c4\u200d\u2642\ufe0f",       // 🏄‍♂️ :surfer:
+	"\U0001f3ca\u200d\u2642\ufe0f",       // 🏊‍♂️ :swimmer:
+	"\U0001f489",                         // 💉 :syringe:
+	"\U0001f389",                         // 🎉 :tada:
+	"\U0001f34a",                         // 🍊 :tangerine:
+	"\U0001f695",                         // 🚕 :taxi:
+	"\U0001f3be",                         // 🎾 :tennis:
+	"\u26fa",                             // ⛺ :tent:
+	"\U0001f4ad",                         // 💭 :thought_balloon:
+	"\u2122\ufe0f",                       // ™️ :tm:
+	"\U0001f6bd",                         // 🚽 :toilet:
+	"\U0001f445",                         // 👅 :tongue:
+	"\U0001f3a9",                         // 🎩 :tophat:
+	"\U0001f69c",                         // 🚜 :tractor:
+	"\U0001f68e",                         // 🚎 :trolleybus:
+	"\U0001f922",                         // 🤢 :nauseated_face:
+	"\U0001f3c6",                         // 🏆 :trophy:
+	"\U0001f3ba",                         // 🎺 :trumpet:
+	"\U0001f422",                         // 🐢 :turtle:
+	"\U0001f3a0",                         // 🎠 :carousel_horse:
+	"\U0001f46d",                         // 👭 :two_women_holding_hands:
+	"\U0001f1ec\U0001f1e7",               // 🇬🇧 :uk:
+	"\u2602\ufe0f",                       // ☂️ :umbrella:
+	"\U0001f513",                         // 🔓 :unlock:
+	"\U0001f1fa\U0001f1f8",               // 🇺🇸 :us:
+	"\u270c\ufe0f",                       // ✌️ :v:
+	"\U0001f4fc",                         // 📼 :vhs:
+	"\U0001f3bb",                         // 🎻 :violin:
+	"\u26a0\ufe0f",                       // ⚠️ :warning:
+	"\U0001f349",                         // 🍉 :watermelon:
+	"\U0001f44b",                         // 👋 :wave:
+	"\u3030\ufe0f",                       // 〰️ :wavy_dash:
+	"\U0001f6be",                         // 🚾 :wc:
+	"\u267f",                             // ♿ :wheelchair:
+	"\U0001f469",                         // 👩 :woman:
+	"\u274c",                             // ❌ :x:
+	"\U0001f60b",                         // 😋 :yum:
+	"\u26a1",                             // ⚡ :zap:
+	"\U0001f4a4",                         // 💤 :zzz:
+}
diff --git a/sshutil/fingerprint.go b/sshutil/fingerprint.go
new file mode 100644
index 0000000..d66cc98
--- /dev/null
+++ b/sshutil/fingerprint.go
@@ -0,0 +1,150 @@
+package sshutil
+
+import (
+	"crypto/dsa" //nolint:staticcheck // support for DSA fingerprints
+	"crypto/rsa"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/hex"
+	"fmt"
+	"strings"
+
+	"github.com/pkg/errors"
+	"go.step.sm/crypto/internal/emoji"
+	"golang.org/x/crypto/ssh"
+)
+
+// FingerprintEncoding defines the supported encodings for SSH key and
+// certificate fingerprints.
+type FingerprintEncoding int
+
+// Supported fingerprint encodings.
+const (
+	// Base64RawFingerprint represents the base64RawStd encoding of the
+	// fingerprint. This is the default encoding for an SSH key.
+	Base64RawFingerprint FingerprintEncoding = iota
+	// Base64RawURLFingerprint represents the base64RawURL encoding of the
+	// fingerprint.
+	Base64RawURLFingerprint
+	// Base64Fingerprint represents the base64 encoding of the fingerprint.
+	Base64Fingerprint
+	// Base64UrlFingerprint represents the base64URL encoding of the
+	// fingerprint.
+	Base64UrlFingerprint FingerprintEncoding = iota
+	// HexFingerprint represents the hex encoding of the fingerprint.
+	HexFingerprint
+	// EmojiFingerprint represents the emoji encoding of the fingerprint.
+	EmojiFingerprint
+)
+
+// Fingerprint returns the SHA-256 fingerprint of an ssh public key or
+// certificate.
+func Fingerprint(pub ssh.PublicKey) string {
+	return EncodedFingerprint(pub, Base64RawFingerprint)
+}
+
+// EncodedFingerprint returns the SHA-256 hash of an ssh public key or
+// certificate using the specified encoding. If an invalid encoding is passed,
+// the return value will be an empty string.
+func EncodedFingerprint(pub ssh.PublicKey, encoding FingerprintEncoding) string {
+	const prefix = "SHA256:"
+
+	sum := sha256.Sum256(pub.Marshal())
+	switch encoding {
+	case Base64RawFingerprint:
+		return prefix + base64.RawStdEncoding.EncodeToString(sum[:])
+	case Base64RawURLFingerprint:
+		return prefix + base64.RawURLEncoding.EncodeToString(sum[:])
+	case Base64Fingerprint:
+		return prefix + base64.StdEncoding.EncodeToString(sum[:])
+	case Base64UrlFingerprint:
+		return prefix + base64.URLEncoding.EncodeToString(sum[:])
+	case HexFingerprint:
+		return prefix + strings.ToLower(hex.EncodeToString(sum[:]))
+	case EmojiFingerprint:
+		return prefix + emoji.Emoji(sum[:])
+	default:
+		return ""
+	}
+}
+
+// FormatFingerprint parses a public key from an authorized_keys file used in
+// OpenSSH and returns a public key fingerprint in the following format:
+//
+//	<size> SHA256:<base64-raw-fingerprint> <comment> (<type)
+func FormatFingerprint(in []byte, encoding FingerprintEncoding) (string, error) {
+	key, comment, _, _, err := ssh.ParseAuthorizedKey(in)
+	if err != nil {
+		return "", fmt.Errorf("error parsing public key: %w", err)
+	}
+	if comment == "" {
+		comment = "no comment"
+	}
+
+	typ, size, err := publicKeyTypeAndSize(key)
+	if err != nil {
+		return "", fmt.Errorf("error determining key type and size: %w", err)
+	}
+
+	fp := EncodedFingerprint(key, encoding)
+	if fp == "" {
+		return "", fmt.Errorf("unsupported encoding format %v", encoding)
+	}
+
+	return fmt.Sprintf("%d %s %s (%s)", size, fp, comment, typ), nil
+}
+
+func publicKeyTypeAndSize(key ssh.PublicKey) (string, int, error) {
+	var isCert bool
+	if cert, ok := key.(*ssh.Certificate); ok {
+		key = cert.Key
+		isCert = true
+	}
+
+	var typ string
+	var size int
+	switch key.Type() {
+	case ssh.KeyAlgoECDSA256:
+		typ, size = "ECDSA", 256
+	case ssh.KeyAlgoECDSA384:
+		typ, size = "ECDSA", 384
+	case ssh.KeyAlgoECDSA521:
+		typ, size = "ECDSA", 521
+	case ssh.KeyAlgoSKECDSA256:
+		typ, size = "SK-ECDSA", 256
+	case ssh.KeyAlgoED25519:
+		typ, size = "ED25519", 256
+	case ssh.KeyAlgoSKED25519:
+		typ, size = "SK-ED25519", 256
+	case ssh.KeyAlgoRSA:
+		typ = "RSA"
+		cpk, err := CryptoPublicKey(key)
+		if err != nil {
+			return "", 0, err
+		}
+		k, ok := cpk.(*rsa.PublicKey)
+		if !ok {
+			return "", 0, errors.New("unsupported key: not an RSA public key")
+		}
+		size = 8 * k.Size()
+	case ssh.KeyAlgoDSA:
+		typ = "DSA"
+		cpk, err := CryptoPublicKey(key)
+		if err != nil {
+			return "", 0, err
+		}
+		k, ok := cpk.(*dsa.PublicKey)
+		if !ok {
+			return "", 0, errors.New("unsupported key: not a DSA public key")
+		}
+		size = k.Parameters.P.BitLen()
+	default:
+		return "", 0, errors.Errorf("public key %s is not supported", key.Type())
+	}
+
+	if isCert {
+		typ += "-CERT"
+	}
+
+	return typ, size, nil
+}
diff --git a/sshutil/sshutil.go b/sshutil/sshutil.go
index 870ded3..c309c2d 100644
--- a/sshutil/sshutil.go
+++ b/sshutil/sshutil.go
@@ -2,6 +2,7 @@ package sshutil
 
 import (
 	"crypto"
+	"crypto/dsa" //nolint:staticcheck // support for DSA fingerprints
 	"crypto/ecdsa"
 	"crypto/ed25519"
 	"crypto/elliptic"
@@ -16,7 +17,7 @@ import (
 // *agent.Key.
 func CryptoPublicKey(pub interface{}) (crypto.PublicKey, error) {
 	switch p := pub.(type) {
-	case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey:
+	case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey, *dsa.PublicKey:
 		return pub, nil
 	case ssh.CryptoPublicKey:
 		return p.CryptoPublicKey(), nil
diff --git a/x509util/utils.go b/x509util/utils.go
index ddb09e5..a8575d9 100644
--- a/x509util/utils.go
+++ b/x509util/utils.go
@@ -19,18 +19,29 @@ import (
 	"unicode/utf8"
 
 	"github.com/pkg/errors"
+	"go.step.sm/crypto/internal/emoji"
 	"golang.org/x/net/idna"
 )
 
-// FingerprintEncoding defines the supported encodigns in certificate
+// FingerprintEncoding defines the supported encodings in certificate
 // fingerprints.
 type FingerprintEncoding int
 
 // Supported fingerprint encodings.
 const (
+	// HexFingerprint represents the hex encoding of the fingerprint. This is
+	// the default encoding for an X.509 certificate.
 	HexFingerprint FingerprintEncoding = iota
+	// Base64Fingerprint represents the base64 encoding of the fingerprint.
 	Base64Fingerprint
+	// Base64UrlFingerprint represents the base64URL encoding of the fingerprint.
 	Base64UrlFingerprint
+	// Base64RawFingerprint represents the base64RawStd encoding of the fingerprint.
+	Base64RawFingerprint
+	// Base64RawURLFingerprint represents the base64RawURL encoding of the fingerprint.
+	Base64RawURLFingerprint
+	// EmojiFingerprint represents the emoji encoding of the fingerprint.
+	EmojiFingerprint
 )
 
 var emptyASN1Subject = []byte{0x30, 0}
@@ -107,9 +118,9 @@ func Fingerprint(cert *x509.Certificate) string {
 	return EncodedFingerprint(cert, HexFingerprint)
 }
 
-// EncodedFingerprint returns an encoded the SHA-256 fingerprint of the
-// certificate using the specified encoding. In an invalid encoding is passed,
-// the return value will be an empty string.
+// EncodedFingerprint returns the SHA-256 hash of the certificate using the
+// specified encoding. If an invalid encoding is passed, the return value will
+// be an empty string.
 func EncodedFingerprint(cert *x509.Certificate, encoding FingerprintEncoding) string {
 	sum := sha256.Sum256(cert.Raw)
 	switch encoding {
@@ -119,6 +130,12 @@ func EncodedFingerprint(cert *x509.Certificate, encoding FingerprintEncoding) st
 		return base64.StdEncoding.EncodeToString(sum[:])
 	case Base64UrlFingerprint:
 		return base64.URLEncoding.EncodeToString(sum[:])
+	case Base64RawFingerprint:
+		return base64.RawStdEncoding.EncodeToString(sum[:])
+	case Base64RawURLFingerprint:
+		return base64.RawURLEncoding.EncodeToString(sum[:])
+	case EmojiFingerprint:
+		return emoji.Emoji(sum[:])
 	default:
 		return ""
 	}


Test patch:
diff --git a/internal/emoji/emoji_test.go b/internal/emoji/emoji_test.go
new file mode 100644
index 0000000..e9cba99
--- /dev/null
+++ b/internal/emoji/emoji_test.go
@@ -0,0 +1,25 @@
+package emoji
+
+import "testing"
+
+func TestEmoji(t *testing.T) {
+	type args struct {
+		input []byte
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{"ok", args{[]byte{0x00, 0x10, 0x50, 0xAA, 0xFF}}, "👍🛁🇫🇷👛💤"},
+		{"empty", args{[]byte{}}, ""},
+		{"nil", args{nil}, ""},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := Emoji(tt.args.input); got != tt.want {
+				t.Errorf("Emoji() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/sshutil/fingerprint_test.go b/sshutil/fingerprint_test.go
new file mode 100644
index 0000000..b344b4b
--- /dev/null
+++ b/sshutil/fingerprint_test.go
@@ -0,0 +1,246 @@
+package sshutil
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/dsa" //nolint:staticcheck // support for DSA fingerprints
+	"crypto/rand"
+	"encoding/base64"
+	"encoding/hex"
+	"testing"
+
+	"go.step.sm/crypto/internal/emoji"
+	"golang.org/x/crypto/ssh"
+)
+
+func generateCertificate(t *testing.T) ssh.PublicKey {
+	t.Helper()
+	key, signer := mustGenerateKey(t)
+
+	cert, err := CreateCertificate(&ssh.Certificate{
+		Nonce:           []byte("0123456789"),
+		Key:             key,
+		Serial:          123,
+		CertType:        ssh.HostCert,
+		KeyId:           "foo",
+		ValidPrincipals: []string{"foo.internal"},
+		ValidAfter:      1111,
+		ValidBefore:     2222,
+		Permissions:     ssh.Permissions{},
+		Reserved:        []byte("reserved"),
+	}, signer)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return cert
+}
+
+func TestFingerprint(t *testing.T) {
+	ecKey, sshECKey := generateKey(t, "EC", "P-256", 0)
+	edKey, sshEDKey := generateKey(t, "OKP", "Ed25519", 0)
+	_, sshRSAKey := generateKey(t, "RSA", "", 2048)
+
+	skECKey := generateFakeSKKey(t, ecKey)
+	skEDKey := generateFakeSKKey(t, edKey)
+	sshCert := generateCertificate(t)
+
+	type args struct {
+		pub ssh.PublicKey
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{"ok ECDSA", args{sshECKey}, ssh.FingerprintSHA256(sshECKey)},
+		{"ok ED25519", args{sshEDKey}, ssh.FingerprintSHA256(sshEDKey)},
+		{"ok RSA", args{sshRSAKey}, ssh.FingerprintSHA256(sshRSAKey)},
+		{"ok SK-ECDSA", args{skECKey}, ssh.FingerprintSHA256(skECKey)},
+		{"ok SK-ED25519", args{skEDKey}, ssh.FingerprintSHA256(skEDKey)},
+		{"ok CERT", args{sshCert}, ssh.FingerprintSHA256(sshCert)},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := Fingerprint(tt.args.pub); got != tt.want {
+				t.Errorf("Fingerprint() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestEncodedFingerprint(t *testing.T) {
+	_, sshECKey := generateKey(t, "EC", "P-256", 0)
+
+	expected := ssh.FingerprintSHA256(sshECKey)
+	b, err := base64.RawStdEncoding.DecodeString(expected[7:])
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type args struct {
+		pub      ssh.PublicKey
+		encoding FingerprintEncoding
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{"ok", args{sshECKey, 0}, ssh.FingerprintSHA256(sshECKey)},
+		{"Base64RawFingerprint", args{sshECKey, Base64RawFingerprint}, expected},
+		{"Base64RawURLFingerprint", args{sshECKey, Base64RawURLFingerprint}, "SHA256:" + base64.RawURLEncoding.EncodeToString(b)},
+		{"Base64Fingerprint", args{sshECKey, Base64Fingerprint}, "SHA256:" + base64.StdEncoding.EncodeToString(b)},
+		{"Base64UrlFingerprint", args{sshECKey, Base64UrlFingerprint}, "SHA256:" + base64.URLEncoding.EncodeToString(b)},
+		{"HexFingerprint", args{sshECKey, HexFingerprint}, "SHA256:" + hex.EncodeToString(b)},
+		{"EmojiFingerprint", args{sshECKey, EmojiFingerprint}, "SHA256:" + emoji.Emoji(b)},
+		{"fail", args{sshECKey, 100}, ""},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := EncodedFingerprint(tt.args.pub, tt.args.encoding); got != tt.want {
+				t.Errorf("EncodedFingerprint() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestFormatFingerprint(t *testing.T) {
+	ecKey, sshECKey := generateKey(t, "EC", "P-256", 0)
+	_, sshEC384Key := generateKey(t, "EC", "P-384", 0)
+	_, sshEC521Key := generateKey(t, "EC", "P-521", 0)
+	edKey, sshEDKey := generateKey(t, "OKP", "Ed25519", 0)
+	_, sshRSAKey := generateKey(t, "RSA", "", 2048)
+
+	skECKey := generateFakeSKKey(t, ecKey)
+	skEDKey := generateFakeSKKey(t, edKey)
+	sshCert := generateCertificate(t)
+
+	dsaKey := new(dsa.PrivateKey)
+	if err := dsa.GenerateParameters(&dsaKey.Parameters, rand.Reader, dsa.L1024N160); err != nil {
+		t.Fatal(err)
+	}
+	if err := dsa.GenerateKey(dsaKey, rand.Reader); err != nil {
+		t.Fatal(err)
+	}
+	dsaSigner, err := ssh.NewSignerFromKey(dsaKey)
+	if err != nil {
+		t.Fatal(err)
+	}
+	sshDSAKey := dsaSigner.PublicKey()
+
+	ec256Bytes, err := base64.RawStdEncoding.DecodeString(ssh.FingerprintSHA256(sshECKey)[7:])
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	marshal := func(pub ssh.PublicKey, comment string) []byte {
+		b := &bytes.Buffer{}
+		b.WriteString(pub.Type())
+		b.WriteByte(' ')
+		e := base64.NewEncoder(base64.StdEncoding, b)
+		_, _ = e.Write(pub.Marshal())
+		e.Close()
+		if comment != "" {
+			b.WriteString(" " + comment)
+		}
+		b.WriteByte('\n')
+		return b.Bytes()
+	}
+
+	type args struct {
+		in       []byte
+		encoding FingerprintEncoding
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		wantErr bool
+	}{
+		{"P256", args{marshal(sshECKey, "jane@example.com"), 0}, "256 " + ssh.FingerprintSHA256(sshECKey) + " jane@example.com (ECDSA)", false},
+		{"P384", args{marshal(sshEC384Key, "jane@example.com"), 0}, "384 " + ssh.FingerprintSHA256(sshEC384Key) + " jane@example.com (ECDSA)", false},
+		{"P521", args{marshal(sshEC521Key, "jane@example.com"), 0}, "521 " + ssh.FingerprintSHA256(sshEC521Key) + " jane@example.com (ECDSA)", false},
+		{"Ed25519", args{marshal(sshEDKey, "jane@example.com"), 0}, "256 " + ssh.FingerprintSHA256(sshEDKey) + " jane@example.com (ED25519)", false},
+		{"RSA", args{marshal(sshRSAKey, "jane@example.com"), 0}, "2048 " + ssh.FingerprintSHA256(sshRSAKey) + " jane@example.com (RSA)", false},
+		{"SK-ECDSA", args{marshal(skECKey, "jane@example.com"), 0}, "256 " + ssh.FingerprintSHA256(skECKey) + " jane@example.com (SK-ECDSA)", false},
+		{"SK-ED25519", args{marshal(skEDKey, "jane@example.com"), 0}, "256 " + ssh.FingerprintSHA256(skEDKey) + " jane@example.com (SK-ED25519)", false},
+		{"ED25519-CERT", args{marshal(sshCert, "jane@example.com"), 0}, "256 " + ssh.FingerprintSHA256(sshCert) + " jane@example.com (ED25519-CERT)", false},
+		{"DSA", args{marshal(sshDSAKey, "jane@example.com"), 0}, "1024 " + ssh.FingerprintSHA256(sshDSAKey) + " jane@example.com (DSA)", false},
+		{"Base64RawFingerprint", args{marshal(sshECKey, ""), Base64RawFingerprint}, "256 " + ssh.FingerprintSHA256(sshECKey) + " no comment (ECDSA)", false},
+		{"Base64RawURLFingerprint", args{marshal(sshECKey, ""), Base64RawURLFingerprint}, "256 SHA256:" + base64.RawURLEncoding.EncodeToString(ec256Bytes) + " no comment (ECDSA)", false},
+		{"Base64Fingerprint", args{marshal(sshECKey, ""), Base64Fingerprint}, "256 SHA256:" + base64.StdEncoding.EncodeToString(ec256Bytes) + " no comment (ECDSA)", false},
+		{"Base64UrlFingerprint", args{marshal(sshECKey, ""), Base64UrlFingerprint}, "256 SHA256:" + base64.URLEncoding.EncodeToString(ec256Bytes) + " no comment (ECDSA)", false},
+		{"HexFingerprint", args{marshal(sshECKey, ""), HexFingerprint}, "256 SHA256:" + hex.EncodeToString(ec256Bytes) + " no comment (ECDSA)", false},
+		{"EmojiFingerprint", args{marshal(sshECKey, ""), EmojiFingerprint}, "256 SHA256:" + emoji.Emoji(ec256Bytes) + " no comment (ECDSA)", false},
+		{"fail input", args{marshal(sshECKey, "")[:50], EmojiFingerprint}, "", true},
+		{"fail encoding", args{marshal(sshECKey, ""), 100}, "", true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := FormatFingerprint(tt.args.in, tt.args.encoding)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("FormatFingerprint() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("FormatFingerprint() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+type fakeKey struct {
+	typ   string
+	bytes []byte
+}
+
+func (s *fakeKey) Type() string                                 { return s.typ }
+func (s *fakeKey) Marshal() []byte                              { return s.bytes }
+func (s *fakeKey) Verify(data []byte, sig *ssh.Signature) error { return nil }
+
+type fakeCryptoPublicKey struct {
+	typ   string
+	key   crypto.PublicKey
+	bytes []byte
+}
+
+func (s *fakeCryptoPublicKey) Type() string                                 { return s.typ }
+func (s *fakeCryptoPublicKey) Marshal() []byte                              { return s.bytes }
+func (s *fakeCryptoPublicKey) Verify(data []byte, sig *ssh.Signature) error { return nil }
+func (s *fakeCryptoPublicKey) CryptoPublicKey() crypto.PublicKey            { return s.key }
+
+func Test_publicKeyTypeAndSize_errors(t *testing.T) {
+	ecKey, _ := generateKey(t, "EC", "P-256", 0)
+
+	type args struct {
+		key ssh.PublicKey
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		want1   int
+		wantErr bool
+	}{
+		{"fail RSA", args{&fakeKey{ssh.KeyAlgoRSA, nil}}, "", 0, true},
+		{"fail DSA", args{&fakeKey{ssh.KeyAlgoDSA, nil}}, "", 0, true},
+		{"fail RSA cast", args{&fakeCryptoPublicKey{ssh.KeyAlgoRSA, ecKey, nil}}, "", 0, true},
+		{"fail DSA cast", args{&fakeCryptoPublicKey{ssh.KeyAlgoDSA, ecKey, nil}}, "", 0, true},
+		{"fail type", args{&fakeKey{"unknown-type", nil}}, "", 0, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, got1, err := publicKeyTypeAndSize(tt.args.key)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("publicKeyTypeAndSize() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("publicKeyTypeAndSize() got = %v, want %v", got, tt.want)
+			}
+			if got1 != tt.want1 {
+				t.Errorf("publicKeyTypeAndSize() got1 = %v, want %v", got1, tt.want1)
+			}
+		})
+	}
+}
diff --git a/sshutil/sshutil_test.go b/sshutil/sshutil_test.go
index e94ae7b..2e3b4ed 100644
--- a/sshutil/sshutil_test.go
+++ b/sshutil/sshutil_test.go
@@ -43,7 +43,7 @@ func generateFakeSKKey(t *testing.T, pub crypto.PublicKey) ssh.PublicKey {
 			ID          string
 			Key         []byte
 			Application string
-		}{"name", "id", elliptic.Marshal(k.Curve, k.X, k.Y), "ssh"}
+		}{"sk-ecdsa-sha2-nistp256@openssh.com", "nistp256", elliptic.Marshal(k.Curve, k.X, k.Y), "ssh"}
 		return &skKey{
 			typ:   "sk-ecdsa-sha2-nistp256@openssh.com",
 			bytes: ssh.Marshal(w),
@@ -53,7 +53,7 @@ func generateFakeSKKey(t *testing.T, pub crypto.PublicKey) ssh.PublicKey {
 			Name        string
 			KeyBytes    []byte
 			Application string
-		}{"name", []byte(k), "ssh"}
+		}{"sk-ssh-ed25519@openssh.com", []byte(k), "ssh"}
 		return &skKey{
 			typ:   "sk-ssh-ed25519@openssh.com",
 			bytes: ssh.Marshal(w),
diff --git a/x509util/utils_test.go b/x509util/utils_test.go
index 67db026..b05e052 100644
--- a/x509util/utils_test.go
+++ b/x509util/utils_test.go
@@ -184,6 +184,9 @@ func TestEncodedFingerprint(t *testing.T) {
 		{"HexFingerprint", args{ecdsaCrt, HexFingerprint}, "38011621ecdcc2172e933a1ef2317efc535a161c00333aee3f84abfab4e640bf"},
 		{"Base64Fingerprint", args{ecdsaCrt, Base64Fingerprint}, "OAEWIezcwhcukzoe8jF+/FNaFhwAMzruP4Sr+rTmQL8="},
 		{"Base64UrlFingerprint", args{ecdsaCrt, Base64UrlFingerprint}, "OAEWIezcwhcukzoe8jF-_FNaFhwAMzruP4Sr-rTmQL8="},
+		{"Base64RawFingerprint", args{ecdsaCrt, Base64RawFingerprint}, "OAEWIezcwhcukzoe8jF+/FNaFhwAMzruP4Sr+rTmQL8"},
+		{"Base64RawURLFingerprint", args{ecdsaCrt, Base64RawURLFingerprint}, "OAEWIezcwhcukzoe8jF-_FNaFhwAMzruP4Sr-rTmQL8"},
+		{"EmojiFingerprint", args{ecdsaCrt, EmojiFingerprint}, "💨🎱🌼📆🎠🎉🎿🚙🍪🌊♦️💡✌️🐮🔒❌🖕😬🌼👦👍👑♦️🇬🇧👂🔬📌♿🚀🚜🍆🐑"},
 		{"Unknown", args{ecdsaCrt, 100}, ""},
 	}
 	for _, tt := range tests {