Skip to content

Commit

Permalink
ssh: reject unencrypted keys from ParsePrivateKeyWithPassphrase
Browse files Browse the repository at this point in the history
The behavior of ParsePrivateKeyWithPassphrase when the key is
unencrypted is unspecified. Currently, it just parses them like
ParsePrivateKey, which is unlikely to be what anyone wants: for us to
ignore a passphrase that they explicitly passed. It also makes the
implementation of encrypted OpenSSH keys in the next CL more confused.

Instead, make ParsePrivateKey return a PassphraseNeededError, so the
application logic can be ParsePrivateKey -> detect encrypted key ->
obtain passphrase -> ParsePrivateKeyWithPassphrase. That error will also
let us return the public key for OpenSSH keys.

Change-Id: Ife4fb2499ae538bef36e353adf9bc8e902662386
  • Loading branch information
FiloSottile committed Nov 18, 2019
1 parent 497ca9f commit 178fe9c
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 22 deletions.
48 changes: 29 additions & 19 deletions ssh/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,8 @@ func NewPublicKey(key interface{}) (PublicKey, error) {
}

// ParsePrivateKey returns a Signer from a PEM encoded private key. It supports
// the same keys as ParseRawPrivateKey.
// the same keys as ParseRawPrivateKey. If the private key is encrypted, it
// will return a PassphraseNeededError.
func ParsePrivateKey(pemBytes []byte) (Signer, error) {
key, err := ParseRawPrivateKey(pemBytes)
if err != nil {
Expand All @@ -850,8 +851,8 @@ func ParsePrivateKey(pemBytes []byte) (Signer, error) {
// ParsePrivateKeyWithPassphrase returns a Signer from a PEM encoded private
// key and passphrase. It supports the same keys as
// ParseRawPrivateKeyWithPassphrase.
func ParsePrivateKeyWithPassphrase(pemBytes, passPhrase []byte) (Signer, error) {
key, err := ParseRawPrivateKeyWithPassphrase(pemBytes, passPhrase)
func ParsePrivateKeyWithPassphrase(pemBytes, passphrase []byte) (Signer, error) {
key, err := ParseRawPrivateKeyWithPassphrase(pemBytes, passphrase)
if err != nil {
return nil, err
}
Expand All @@ -867,16 +868,29 @@ func encryptedBlock(block *pem.Block) bool {
return strings.Contains(block.Headers["Proc-Type"], "ENCRYPTED")
}

// A PassphraseNeededError indicates that parsing this private key requires a
// passphrase. Use ParsePrivateKeyWithPassphrase.
type PassphraseNeededError struct {
// PublicKey will be set if the private key format includes an unencrypted
// public key along with the encrypted private key.
PublicKey PublicKey
}

func (*PassphraseNeededError) Error() string {
return "ssh: this private key is passphrase protected"
}

// ParseRawPrivateKey returns a private key from a PEM encoded private key. It
// supports RSA (PKCS#1), PKCS#8, DSA (OpenSSL), and ECDSA private keys.
// supports RSA (PKCS#1), PKCS#8, DSA (OpenSSL), and ECDSA private keys. If the
// private key is encrypted, it will return a PassphraseNeededError.
func ParseRawPrivateKey(pemBytes []byte) (interface{}, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("ssh: no key found")
}

if encryptedBlock(block) {
return nil, errors.New("ssh: cannot decode encrypted private keys")
return nil, &PassphraseNeededError{}
}

switch block.Type {
Expand All @@ -899,24 +913,22 @@ func ParseRawPrivateKey(pemBytes []byte) (interface{}, error) {
// ParseRawPrivateKeyWithPassphrase returns a private key decrypted with
// passphrase from a PEM encoded private key. If wrong passphrase, return
// x509.IncorrectPasswordError.
func ParseRawPrivateKeyWithPassphrase(pemBytes, passPhrase []byte) (interface{}, error) {
func ParseRawPrivateKeyWithPassphrase(pemBytes, passphrase []byte) (interface{}, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("ssh: no key found")
}
buf := block.Bytes

if encryptedBlock(block) {
if x509.IsEncryptedPEMBlock(block) {
var err error
buf, err = x509.DecryptPEMBlock(block, passPhrase)
if err != nil {
if err == x509.IncorrectPasswordError {
return nil, err
}
return nil, fmt.Errorf("ssh: cannot decode encrypted private keys: %v", err)
}
if !encryptedBlock(block) || !x509.IsEncryptedPEMBlock(block) {
return nil, errors.New("ssh: not an encrypted key")
}

buf, err := x509.DecryptPEMBlock(block, passphrase)
if err != nil {
if err == x509.IncorrectPasswordError {
return nil, err
}
return nil, fmt.Errorf("ssh: cannot decode encrypted private keys: %v", err)
}

switch block.Type {
Expand All @@ -926,8 +938,6 @@ func ParseRawPrivateKeyWithPassphrase(pemBytes, passPhrase []byte) (interface{},
return x509.ParseECPrivateKey(buf)
case "DSA PRIVATE KEY":
return ParseDSAPrivateKey(buf)
case "OPENSSH PRIVATE KEY":
return parseOpenSSHPrivateKey(buf)
default:
return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type)
}
Expand Down
5 changes: 2 additions & 3 deletions ssh/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,15 @@ func TestParseECPrivateKey(t *testing.T) {

// See Issue https://github.com/golang/go/issues/6650.
func TestParseEncryptedPrivateKeysFails(t *testing.T) {
const wantSubstring = "encrypted"
for i, tt := range testdata.PEMEncryptedKeys {
_, err := ParsePrivateKey(tt.PEMBytes)
if err == nil {
t.Errorf("#%d key %s: ParsePrivateKey successfully parsed, expected an error", i, tt.Name)
continue
}

if !strings.Contains(err.Error(), wantSubstring) {
t.Errorf("#%d key %s: got error %q, want substring %q", i, tt.Name, err, wantSubstring)
if _, ok := err.(*PassphraseNeededError); !ok {
t.Errorf("#%d key %s: got error %q, want PassphraseNeededError", i, tt.Name, err)
}
}
}
Expand Down

0 comments on commit 178fe9c

Please sign in to comment.