diff --git a/ssh/keys.go b/ssh/keys.go index 78f0963..5deada7 100644 --- a/ssh/keys.go +++ b/ssh/keys.go @@ -1466,6 +1466,17 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc { return nil, err } + // OpenSSH does not impose an upper bound on the bcrypt round count + // stored in the key file, but bcrypt_pbkdf cost is linear in rounds: + // the default is 16, ssh-keygen lets users pick anything up to + // INT_MAX. Cap at 2048 (128x the default, a few seconds of CPU) so + // that an oversized value in the file cannot tie up the caller for + // months. + const maxRounds = 1 << 11 + if opts.Rounds > maxRounds { + return nil, fmt.Errorf("ssh: bcrypt KDF rounds %d exceed maximum %d", opts.Rounds, maxRounds) + } + k, err := bcrypt_pbkdf.Key(passphrase, []byte(opts.Salt), int(opts.Rounds), 32+16) if err != nil { return nil, err diff --git a/ssh/keys_test.go b/ssh/keys_test.go index c973c8c..9bd90a0 100644 --- a/ssh/keys_test.go +++ b/ssh/keys_test.go @@ -506,6 +506,38 @@ func TestParseEncryptedPrivateKeysWithIncorrectPassphrase(t *testing.T) { } } +func TestParseEncryptedPrivateKeyExcessiveBcryptRounds(t *testing.T) { + // Craft a minimal openssh-key-v1 blob whose KdfOpts declares a bcrypt + // round count above the accepted maximum. The check must reject the file + // before bcrypt_pbkdf is invoked, so the rest of the blob (public key, + // encrypted body) can be empty. + kdfOpts := Marshal(struct { + Salt []byte + Rounds uint32 + }{ + Salt: []byte("salt-not-used"), + Rounds: (1 << 11) + 1, + }) + header := Marshal(openSSHEncryptedPrivateKey{ + CipherName: "aes256-ctr", + KdfName: "bcrypt", + KdfOpts: string(kdfOpts), + NumKeys: 1, + }) + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "OPENSSH PRIVATE KEY", + Bytes: append([]byte(privateKeyAuthMagic), header...), + }) + + _, err := ParseRawPrivateKeyWithPassphrase(pemBytes, []byte("password")) + if err == nil { + t.Fatal("expected error for excessive bcrypt rounds, got nil") + } + if !strings.Contains(err.Error(), "bcrypt KDF rounds") { + t.Errorf("got error %q, want substring %q", err.Error(), "bcrypt KDF rounds") + } +} + func TestParseDSA(t *testing.T) { // We actually exercise the ParsePrivateKey codepath here, as opposed to // using the ParseRawPrivateKey+NewSignerFromKey path that testdata_test.go