From 9ee94185fcd74e4a162a2f3ebdb3dd01993fd595 Mon Sep 17 00:00:00 2001 From: Santhanam Date: Sun, 5 Oct 2025 23:31:27 +0530 Subject: [PATCH 1/5] ssh: fix error message on unsupported cipher Fixes golang/go#52135 Until now, when ssh keys using one of these[1] ciphers were passed, we were giving a parse error "ssh: parse error in message type 0". With this fix, we parse it successfully and return the correct error message. [1] aes{128,256}-gcm@openssh.com and chacha20-poly1305@openssh.com --- ssh/keys.go | 15 +++++++++++- ssh/keys_test.go | 16 +++++++++++++ ssh/testdata/keys.go | 55 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/ssh/keys.go b/ssh/keys.go index a035956fcc..69947fb710 100644 --- a/ssh/keys.go +++ b/ssh/keys.go @@ -1271,6 +1271,15 @@ func (*PassphraseMissingError) Error() string { return "ssh: this private key is passphrase protected" } +type UnsupportedCipherError struct { + BadCipher string + SupportedCiphers []string +} + +func (e *UnsupportedCipherError) Error() string { + return fmt.Sprintf("ssh: unknown cipher %q, only supports one of %q", e.BadCipher, strings.Join(e.SupportedCiphers, ",")) +} + // ParseRawPrivateKey returns a private key from a PEM encoded private key. It supports // RSA, DSA, ECDSA, and Ed25519 private keys in PKCS#1, PKCS#8, OpenSSL, and OpenSSH // formats. If the private key is encrypted, it will return a PassphraseMissingError. @@ -1429,7 +1438,10 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc { cbc := cipher.NewCBCDecrypter(c, iv) cbc.CryptBlocks(privKeyBlock, privKeyBlock) default: - return nil, fmt.Errorf("ssh: unknown cipher %q, only supports %q or %q", cipherName, "aes256-ctr", "aes256-cbc") + return nil, &UnsupportedCipherError{ + BadCipher: cipherName, + SupportedCiphers: []string{"aes256-ctr", "aes256-cbc"}, + } } return privKeyBlock, nil @@ -1490,6 +1502,7 @@ type openSSHEncryptedPrivateKey struct { NumKeys uint32 PubKey []byte PrivKeyBlock []byte + Rest []byte `ssh:"rest"` } type openSSHPrivateKey struct { diff --git a/ssh/keys_test.go b/ssh/keys_test.go index 661e3cb31c..11104431bf 100644 --- a/ssh/keys_test.go +++ b/ssh/keys_test.go @@ -271,6 +271,22 @@ func TestParseEncryptedPrivateKeysWithPassphrase(t *testing.T) { } } +func TestParseEncryptedPrivateKeysWithUnsupportedCiphers(t *testing.T) { + for _, tt := range testdata.PEMEncryptedKeysForUnsupportedCiphers { + t.Run(tt.Name, func(t *testing.T) { + _, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey)) + var e *UnsupportedCipherError + if !errors.As(err, &e) { + t.Errorf("got error %v, want PassphraseMissingError", err) + } + + if e.BadCipher != tt.Cipher { + t.Errorf("got badcipher %q, wanted %q", e.BadCipher, tt.Cipher) + } + }) + } +} + func TestParseEncryptedPrivateKeysWithIncorrectPassphrase(t *testing.T) { pem := testdata.PEMEncryptedKeys[0].PEMBytes for i := 0; i < 4096; i++ { diff --git a/ssh/testdata/keys.go b/ssh/testdata/keys.go index 6e48841b67..8eb40bac1c 100644 --- a/ssh/testdata/keys.go +++ b/ssh/testdata/keys.go @@ -216,12 +216,15 @@ var SSHCertificates = map[string][]byte{ `), } -var PEMEncryptedKeys = []struct { +type PEMEncryptedKey struct { Name string EncryptionKey string IncludesPublicKey bool + Cipher string PEMBytes []byte -}{ +} + +var PEMEncryptedKeys = []PEMEncryptedKey{ 0: { Name: "rsa-encrypted", EncryptionKey: "r54-G0pher_t3st$", @@ -310,6 +313,54 @@ gbDGyT3bXMQtagvCwoW+/oMTKXiZP5jCJpEO8= }, } +var PEMEncryptedKeysForUnsupportedCiphers = []PEMEncryptedKey{ + 0: { + Name: "ed25519-encrypted-chacha20-poly1305", + EncryptionKey: "password", + IncludesPublicKey: true, + Cipher: "chacha20-poly1305@openssh.com", + PEMBytes: []byte(`-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAAHWNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc2guY29tAAAABm +JjcnlwdAAAABgAAAAQdPyPIjXDRAVHskY0yp9SWwAAAGQAAAABAAAAMwAAAAtzc2gtZWQy +NTUxOQAAACBi6qXITEUrmNce/c2lfozxALlKH3o/6sll8G7wzl1lvQAAAJDNlW1sEkvnK0 +8EecF1vHdPk85yClbh3KkHv09mbGAX/Gk6cJpYEGgJSkO7OEF4kG9DVGGd17+TZbTnM4LD +vYAJZExx2XLgJFEtHCVmJjYzwxx7yC7+s6u/XjrSlZS60RHunOPKyq+C+s48sejXvmX+t5 +0ZoVCI8aftT0ycis3gvLU9sCwJ2UnF6kAV226Z4g2aLkuJbgCDTEcYCRD64K1r +-----END OPENSSH PRIVATE KEY----- +`), + }, + 1: { + Name: "ed25519-encrypted-aes128-gcm", + EncryptionKey: "password", + IncludesPublicKey: true, + Cipher: "aes128-gcm@openssh.com", + PEMBytes: []byte(`-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAAFmFlczEyOC1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA +AAGAAAABBeMJIOqiyFwNCvDv6f8tQeAAAAZAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAA +IGYpUcb3tGp9kF6pppcUdq3EPMr85BaSUdhiXGbhS5YNAAAAkNBtMEu0UlLgToThuQc+4m +/o0DfFIERu0sspQivn5RJHCtulVKfU9BMiEnF0+LOMOABMlYesgLOtoMxwm4ZCSWH54kZk +vaFyyvvxY+RLDuWNQZCryffIA4+iLCUQR1EdxMDiJweKnGJuD64a+9xTJt47A3Vq4SYzji +EuVmM0FqS8lbT2ynYSe3va0Qyw13jEO5qbtCuyG+C5GejL7kX4Z64= +-----END OPENSSH PRIVATE KEY----- +`), + }, + 2: { + Name: "ed25519-encrypted-aes256-gcm", + EncryptionKey: "password", + IncludesPublicKey: true, + Cipher: "aes256-gcm@openssh.com", + PEMBytes: []byte(`-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAAFmFlczI1Ni1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA +AAGAAAABBR1p3vH2Wr/HPL+q20L2rjAAAAZAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAA +IM3tT1xrAuOHcrBdoLRo/ojWZsAw2lHfF5hJgFEOts5MAAAAkH/YGrDhDw8u+F8e4P+84B +tAzvp55Lf1Yl7y34BrVmqlWqw/7boqahOp6iYJHNpcuanzc5T6s7Z3wSSYodbY1uvFOfbj +rtP6rIHQIY5J2C40WOYJN8IkZlkwDXwZY0qoE9699ZYmWdwsXRZ7QDhjd2W8ziyZBsttiB +kv2ceuJMLT04TrKc2+RUkj4CQYnz7p8EkgZlUozx8wBSxKFGnkP7k= +-----END OPENSSH PRIVATE KEY----- +`), + }, +} + // SKData contains a list of PubKeys backed by U2F/FIDO2 Security Keys and their test data. var SKData = []struct { Name string From 265bc0e848dc6d6cf8b4036801484dfa500903be Mon Sep 17 00:00:00 2001 From: Santhanam Date: Mon, 6 Oct 2025 00:29:28 +0530 Subject: [PATCH 2/5] fix test error string --- ssh/keys_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh/keys_test.go b/ssh/keys_test.go index 11104431bf..283513f4dc 100644 --- a/ssh/keys_test.go +++ b/ssh/keys_test.go @@ -277,7 +277,7 @@ func TestParseEncryptedPrivateKeysWithUnsupportedCiphers(t *testing.T) { _, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey)) var e *UnsupportedCipherError if !errors.As(err, &e) { - t.Errorf("got error %v, want PassphraseMissingError", err) + t.Errorf("got error %v, want UnsupportedCipherError", err) } if e.BadCipher != tt.Cipher { From 27623e0c6782242d73b9b3faa512aab8cda132b7 Mon Sep 17 00:00:00 2001 From: Santhanam Date: Thu, 9 Oct 2025 14:14:25 +0530 Subject: [PATCH 3/5] make error type private UnsupportedCipherError was exported, for no good reason. It has now been made private. --- ssh/keys.go | 6 +++--- ssh/keys_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ssh/keys.go b/ssh/keys.go index 69947fb710..d0ffc23e80 100644 --- a/ssh/keys.go +++ b/ssh/keys.go @@ -1271,12 +1271,12 @@ func (*PassphraseMissingError) Error() string { return "ssh: this private key is passphrase protected" } -type UnsupportedCipherError struct { +type unsupportedCipherError struct { BadCipher string SupportedCiphers []string } -func (e *UnsupportedCipherError) Error() string { +func (e *unsupportedCipherError) Error() string { return fmt.Sprintf("ssh: unknown cipher %q, only supports one of %q", e.BadCipher, strings.Join(e.SupportedCiphers, ",")) } @@ -1438,7 +1438,7 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc { cbc := cipher.NewCBCDecrypter(c, iv) cbc.CryptBlocks(privKeyBlock, privKeyBlock) default: - return nil, &UnsupportedCipherError{ + return nil, &unsupportedCipherError{ BadCipher: cipherName, SupportedCiphers: []string{"aes256-ctr", "aes256-cbc"}, } diff --git a/ssh/keys_test.go b/ssh/keys_test.go index 283513f4dc..6080b69281 100644 --- a/ssh/keys_test.go +++ b/ssh/keys_test.go @@ -275,7 +275,7 @@ func TestParseEncryptedPrivateKeysWithUnsupportedCiphers(t *testing.T) { for _, tt := range testdata.PEMEncryptedKeysForUnsupportedCiphers { t.Run(tt.Name, func(t *testing.T) { _, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey)) - var e *UnsupportedCipherError + var e *unsupportedCipherError if !errors.As(err, &e) { t.Errorf("got error %v, want UnsupportedCipherError", err) } From 7df89b1f1e3c4a7f83eedd2c344ec96e78a94077 Mon Sep 17 00:00:00 2001 From: Santhanam Date: Sun, 9 Nov 2025 22:54:38 +0530 Subject: [PATCH 4/5] remove error type We remove the error type as requested in review, and keep the existing string error. In the test now, we check using string contains instead. --- ssh/keys.go | 16 ++-------------- ssh/keys_test.go | 25 ++++++++++++------------- ssh/testdata/keys.go | 20 ++++++++------------ 3 files changed, 22 insertions(+), 39 deletions(-) diff --git a/ssh/keys.go b/ssh/keys.go index d0ffc23e80..d4c4178a1c 100644 --- a/ssh/keys.go +++ b/ssh/keys.go @@ -1271,15 +1271,6 @@ func (*PassphraseMissingError) Error() string { return "ssh: this private key is passphrase protected" } -type unsupportedCipherError struct { - BadCipher string - SupportedCiphers []string -} - -func (e *unsupportedCipherError) Error() string { - return fmt.Sprintf("ssh: unknown cipher %q, only supports one of %q", e.BadCipher, strings.Join(e.SupportedCiphers, ",")) -} - // ParseRawPrivateKey returns a private key from a PEM encoded private key. It supports // RSA, DSA, ECDSA, and Ed25519 private keys in PKCS#1, PKCS#8, OpenSSL, and OpenSSH // formats. If the private key is encrypted, it will return a PassphraseMissingError. @@ -1438,10 +1429,7 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc { cbc := cipher.NewCBCDecrypter(c, iv) cbc.CryptBlocks(privKeyBlock, privKeyBlock) default: - return nil, &unsupportedCipherError{ - BadCipher: cipherName, - SupportedCiphers: []string{"aes256-ctr", "aes256-cbc"}, - } + return nil, fmt.Errorf("ssh: unknown cipher %q, only supports %q or %q", cipherName, "aes256-ctr", "aes256-cbc") } return privKeyBlock, nil @@ -1502,7 +1490,7 @@ type openSSHEncryptedPrivateKey struct { NumKeys uint32 PubKey []byte PrivKeyBlock []byte - Rest []byte `ssh:"rest"` + Rest []byte `ssh:"rest"` } type openSSHPrivateKey struct { diff --git a/ssh/keys_test.go b/ssh/keys_test.go index 6080b69281..a1165ec68b 100644 --- a/ssh/keys_test.go +++ b/ssh/keys_test.go @@ -272,19 +272,18 @@ func TestParseEncryptedPrivateKeysWithPassphrase(t *testing.T) { } func TestParseEncryptedPrivateKeysWithUnsupportedCiphers(t *testing.T) { - for _, tt := range testdata.PEMEncryptedKeysForUnsupportedCiphers { - t.Run(tt.Name, func(t *testing.T) { - _, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey)) - var e *unsupportedCipherError - if !errors.As(err, &e) { - t.Errorf("got error %v, want UnsupportedCipherError", err) - } - - if e.BadCipher != tt.Cipher { - t.Errorf("got badcipher %q, wanted %q", e.BadCipher, tt.Cipher) - } - }) - } + for _, tt := range testdata.UnsupportedCipherData { + t.Run(tt.Name, func(t *testing.T){ + _, err := ParsePrivateKeyWithPassphrase(tt.PEMBytes, []byte(tt.EncryptionKey)) + if err == nil { + t.Fatalf("expected 'unknown cipher' error for %q, got nil", tt.Name) + // If this cipher is now supported, remove it from testdata.UnsupportedCipherData + } + if !strings.Contains(err.Error(), "unknown cipher") { + t.Errorf("wanted 'unknown cipher' error, got %v", err.Error()) + } + }) + } } func TestParseEncryptedPrivateKeysWithIncorrectPassphrase(t *testing.T) { diff --git a/ssh/testdata/keys.go b/ssh/testdata/keys.go index 8eb40bac1c..adb4244eb3 100644 --- a/ssh/testdata/keys.go +++ b/ssh/testdata/keys.go @@ -216,15 +216,12 @@ var SSHCertificates = map[string][]byte{ `), } -type PEMEncryptedKey struct { +var PEMEncryptedKeys = []struct { Name string EncryptionKey string IncludesPublicKey bool - Cipher string PEMBytes []byte -} - -var PEMEncryptedKeys = []PEMEncryptedKey{ +}{ 0: { Name: "rsa-encrypted", EncryptionKey: "r54-G0pher_t3st$", @@ -313,12 +310,14 @@ gbDGyT3bXMQtagvCwoW+/oMTKXiZP5jCJpEO8= }, } -var PEMEncryptedKeysForUnsupportedCiphers = []PEMEncryptedKey{ +var UnsupportedCipherData = []struct { + Name string + EncryptionKey string + PEMBytes []byte +} { 0: { Name: "ed25519-encrypted-chacha20-poly1305", EncryptionKey: "password", - IncludesPublicKey: true, - Cipher: "chacha20-poly1305@openssh.com", PEMBytes: []byte(`-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAAHWNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc2guY29tAAAABm JjcnlwdAAAABgAAAAQdPyPIjXDRAVHskY0yp9SWwAAAGQAAAABAAAAMwAAAAtzc2gtZWQy @@ -332,8 +331,6 @@ vYAJZExx2XLgJFEtHCVmJjYzwxx7yC7+s6u/XjrSlZS60RHunOPKyq+C+s48sejXvmX+t5 1: { Name: "ed25519-encrypted-aes128-gcm", EncryptionKey: "password", - IncludesPublicKey: true, - Cipher: "aes128-gcm@openssh.com", PEMBytes: []byte(`-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAAFmFlczEyOC1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA AAGAAAABBeMJIOqiyFwNCvDv6f8tQeAAAAZAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAA @@ -347,8 +344,6 @@ EuVmM0FqS8lbT2ynYSe3va0Qyw13jEO5qbtCuyG+C5GejL7kX4Z64= 2: { Name: "ed25519-encrypted-aes256-gcm", EncryptionKey: "password", - IncludesPublicKey: true, - Cipher: "aes256-gcm@openssh.com", PEMBytes: []byte(`-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAAFmFlczI1Ni1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA AAGAAAABBR1p3vH2Wr/HPL+q20L2rjAAAAZAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAA @@ -361,6 +356,7 @@ kv2ceuJMLT04TrKc2+RUkj4CQYnz7p8EkgZlUozx8wBSxKFGnkP7k= }, } + // SKData contains a list of PubKeys backed by U2F/FIDO2 Security Keys and their test data. var SKData = []struct { Name string From 14ac7e97306d41cba48053b9c60f2ffc7caded45 Mon Sep 17 00:00:00 2001 From: Santhanam Date: Sun, 9 Nov 2025 23:22:09 +0530 Subject: [PATCH 5/5] fix indent --- ssh/keys.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh/keys.go b/ssh/keys.go index d4c4178a1c..47a07539d9 100644 --- a/ssh/keys.go +++ b/ssh/keys.go @@ -1490,7 +1490,7 @@ type openSSHEncryptedPrivateKey struct { NumKeys uint32 PubKey []byte PrivKeyBlock []byte - Rest []byte `ssh:"rest"` + Rest []byte `ssh:"rest"` } type openSSHPrivateKey struct {