Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix SSH certificate fingerprint encoding #207

Merged
merged 5 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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