Skip to content

Commit

Permalink
Merge pull request #207 from smallstep/herman/fix-ssh-certificate-fin…
Browse files Browse the repository at this point in the history
…gerprint

Fix SSH certificate fingerprint encoding
  • Loading branch information
hslatman authored Apr 11, 2023
2 parents 01bdeae + a917ed9 commit c06930f
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 33 deletions.
43 changes: 42 additions & 1 deletion sshutil/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,42 @@ func EncodedFingerprint(pub ssh.PublicKey, encoding FingerprintEncoding) string
// OpenSSH and returns a public key fingerprint in the following format:
//
// <size> SHA256:<base64-raw-fingerprint> <comment> (<type)
//
// If the input is an SSH certificate, its public key will be extracted and
// taken as input for the fingerprint.
func FormatFingerprint(in []byte, encoding FingerprintEncoding) (string, error) {
return formatFingerprint(in, encoding, false)
}

// FormatCertificateFingerprint parses an SSH certificate as used by
// OpenSSH and returns a public key fingerprint in the following format:
//
// <size> SHA256:<base64-raw-fingerprint> <comment> (<type)
//
// If the input is not an SSH certificate, an error will be returned.
func FormatCertificateFingerprint(in []byte, encoding FingerprintEncoding) (string, error) {
return formatFingerprint(in, encoding, true)
}

// formatFingerprint parses a public key from an authorized_keys file or an
// SSH certificate as used by OpenSSH and returns a public key fingerprint
// in the following format:
//
// <size> SHA256:<base64-raw-fingerprint> <comment> (<type)
//
// If the input is an SSH certificate and `asCertificate` is false, the certificate
// public key will be used as input for the fingerprint. If `asCertificate` is true,
// the full contents of the certificate will be used in the fingerprint. If the input
// is not an SSH certificate, but `asCertificate` is true, an error will be returned.
func formatFingerprint(in []byte, encoding FingerprintEncoding, asCertificate bool) (string, error) {
key, comment, _, _, err := ssh.ParseAuthorizedKey(in)
if err != nil {
return "", fmt.Errorf("error parsing public key: %w", err)
}
cert, keyIsCertificate := key.(*ssh.Certificate)
if asCertificate && !keyIsCertificate {
return "", fmt.Errorf("cannot fingerprint SSH key as SSH certificate")
}
if comment == "" {
comment = "no comment"
}
Expand All @@ -76,7 +107,17 @@ func FormatFingerprint(in []byte, encoding FingerprintEncoding) (string, error)
return "", fmt.Errorf("error determining key type and size: %w", err)
}

fp := EncodedFingerprint(key, encoding)
// if the SSH key is actually an SSH certificate and when
// the fingerprint has to be determined for the public key,
// get the public key from the certificate and encode just
// that, instead of encoding the entire key blob including
// certificate bytes.
publicKey := key
if keyIsCertificate && !asCertificate {
publicKey = cert.Key
}

fp := EncodedFingerprint(publicKey, encoding)
if fp == "" {
return "", fmt.Errorf("unsupported encoding format %v", encoding)
}
Expand Down
109 changes: 77 additions & 32 deletions sshutil/fingerprint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/internal/emoji"
"golang.org/x/crypto/ssh"
)
Expand Down Expand Up @@ -104,6 +107,31 @@ func TestEncodedFingerprint(t *testing.T) {
}
}

func marshal(t *testing.T, pub ssh.PublicKey, comment string) []byte {
t.Helper()
b := &bytes.Buffer{}
_, err := b.WriteString(pub.Type())
require.NoError(t, err)
err = b.WriteByte(' ')
require.NoError(t, err)
e := base64.NewEncoder(base64.StdEncoding, b)
_, err = e.Write(pub.Marshal())
require.NoError(t, err)
err = e.Close()
require.NoError(t, err)
if comment != "" {
_, err = b.WriteString(" " + comment)
require.NoError(t, err)
}
err = b.WriteByte('\n')
require.NoError(t, err)
return b.Bytes()
}

const (
fixtureECDSACertificate = `[email protected] AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI herman`
)

func TestFormatFingerprint(t *testing.T) {
ecKey, sshECKey := generateKey(t, "EC", "P-256", 0)
_, sshEC384Key := generateKey(t, "EC", "P-384", 0)
Expand All @@ -114,6 +142,7 @@ func TestFormatFingerprint(t *testing.T) {
skECKey := generateFakeSKKey(t, ecKey)
skEDKey := generateFakeSKKey(t, edKey)
sshCert := generateCertificate(t)
sshCertPublicKey := sshCert.(*ssh.Certificate).Key

dsaKey := new(dsa.PrivateKey)
if err := dsa.GenerateParameters(&dsaKey.Parameters, rand.Reader, dsa.L1024N160); err != nil {
Expand All @@ -132,21 +161,6 @@ func TestFormatFingerprint(t *testing.T) {
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
Expand All @@ -157,23 +171,24 @@ func TestFormatFingerprint(t *testing.T) {
want string
wantErr bool
}{
{"P256", args{marshal(sshECKey, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(sshECKey) + " [email protected] (ECDSA)", false},
{"P384", args{marshal(sshEC384Key, "[email protected]"), 0}, "384 " + ssh.FingerprintSHA256(sshEC384Key) + " [email protected] (ECDSA)", false},
{"P521", args{marshal(sshEC521Key, "[email protected]"), 0}, "521 " + ssh.FingerprintSHA256(sshEC521Key) + " [email protected] (ECDSA)", false},
{"Ed25519", args{marshal(sshEDKey, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(sshEDKey) + " [email protected] (ED25519)", false},
{"RSA", args{marshal(sshRSAKey, "[email protected]"), 0}, "2048 " + ssh.FingerprintSHA256(sshRSAKey) + " [email protected] (RSA)", false},
{"SK-ECDSA", args{marshal(skECKey, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(skECKey) + " [email protected] (SK-ECDSA)", false},
{"SK-ED25519", args{marshal(skEDKey, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(skEDKey) + " [email protected] (SK-ED25519)", false},
{"ED25519-CERT", args{marshal(sshCert, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(sshCert) + " [email protected] (ED25519-CERT)", false},
{"DSA", args{marshal(sshDSAKey, "[email protected]"), 0}, "1024 " + ssh.FingerprintSHA256(sshDSAKey) + " [email protected] (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},
{"P256", args{marshal(t, sshECKey, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(sshECKey) + " [email protected] (ECDSA)", false},
{"P384", args{marshal(t, sshEC384Key, "[email protected]"), 0}, "384 " + ssh.FingerprintSHA256(sshEC384Key) + " [email protected] (ECDSA)", false},
{"P521", args{marshal(t, sshEC521Key, "[email protected]"), 0}, "521 " + ssh.FingerprintSHA256(sshEC521Key) + " [email protected] (ECDSA)", false},
{"Ed25519", args{marshal(t, sshEDKey, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(sshEDKey) + " [email protected] (ED25519)", false},
{"RSA", args{marshal(t, sshRSAKey, "[email protected]"), 0}, "2048 " + ssh.FingerprintSHA256(sshRSAKey) + " [email protected] (RSA)", false},
{"SK-ECDSA", args{marshal(t, skECKey, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(skECKey) + " [email protected] (SK-ECDSA)", false},
{"SK-ED25519", args{marshal(t, skEDKey, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(skEDKey) + " [email protected] (SK-ED25519)", false},
{"ED25519-CERT", args{marshal(t, sshCert, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(sshCertPublicKey) + " [email protected] (ED25519-CERT)", false},
{"ECDSA-CERT (fixture)", args{[]byte(fixtureECDSACertificate), DefaultFingerprint}, "256 SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 herman (ECDSA-CERT)", false},
{"DSA", args{marshal(t, sshDSAKey, "[email protected]"), 0}, "1024 " + ssh.FingerprintSHA256(sshDSAKey) + " [email protected] (DSA)", false},
{"Base64RawFingerprint", args{marshal(t, sshECKey, ""), Base64RawFingerprint}, "256 " + ssh.FingerprintSHA256(sshECKey) + " no comment (ECDSA)", false},
{"Base64RawURLFingerprint", args{marshal(t, sshECKey, ""), Base64RawURLFingerprint}, "256 SHA256:" + base64.RawURLEncoding.EncodeToString(ec256Bytes) + " no comment (ECDSA)", false},
{"Base64Fingerprint", args{marshal(t, sshECKey, ""), Base64Fingerprint}, "256 SHA256:" + base64.StdEncoding.EncodeToString(ec256Bytes) + " no comment (ECDSA)", false},
{"Base64UrlFingerprint", args{marshal(t, sshECKey, ""), Base64URLFingerprint}, "256 SHA256:" + base64.URLEncoding.EncodeToString(ec256Bytes) + " no comment (ECDSA)", false},
{"HexFingerprint", args{marshal(t, sshECKey, ""), HexFingerprint}, "256 SHA256:" + hex.EncodeToString(ec256Bytes) + " no comment (ECDSA)", false},
{"EmojiFingerprint", args{marshal(t, sshECKey, ""), EmojiFingerprint}, "256 SHA256:" + emoji.Emoji(ec256Bytes) + " no comment (ECDSA)", false},
{"fail input", args{marshal(t, sshECKey, "")[:50], EmojiFingerprint}, "", true},
{"fail encoding", args{marshal(t, sshECKey, ""), 100}, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -189,6 +204,36 @@ func TestFormatFingerprint(t *testing.T) {
}
}

func TestFormatCertificateFingerprint(t *testing.T) {
ecKey, sshECKey := generateKey(t, "EC", "P-256", 0)
_ = ecKey
type args struct {
in []byte
encoding FingerprintEncoding
}
tests := []struct {
name string
args args
want string
expErr error
}{
{"P256", args{marshal(t, sshECKey, "[email protected]"), 0}, "256 " + ssh.FingerprintSHA256(sshECKey) + " [email protected] (ECDSA)", errors.New("cannot fingerprint SSH key as SSH certificate")},
{"ECDSA-CERT (fixture)", args{[]byte(fixtureECDSACertificate), DefaultFingerprint}, "256 SHA256:YuV7pyvW7Jp4iEwddOsMw+EdugrhKGqOKh6o+PP0xeg herman (ECDSA-CERT)", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := FormatCertificateFingerprint(tt.args.in, tt.args.encoding)
if tt.expErr != nil {
assert.EqualError(t, err, tt.expErr.Error())
return
}

assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

type fakeKey struct {
typ string
bytes []byte
Expand Down

0 comments on commit c06930f

Please sign in to comment.