fix(detectors/privatekey): report encrypted keys with uncrackable passphrases (#5127)
When a private key is passphrase-protected and Crack cannot recover the passphrase from the built-in wordlist, FromData set a verification error on the result but then continued past the append, dropping the finding entirely. An encrypted key committed to a repository is still a real exposure (offline-crackable, weak or legacy KDFs, and the passphrase is often reused or committed nearby), so it should surface as an unverified finding. Append the result before continuing so the encrypted key is reported as unverified with its verification error and encrypted:true metadata, matching how unencrypted keys are already reported under --no-verification. Closes #5115 Co-authored-by: Shahzad Haider <[email protected]>
This commit is contained in:
co-authored by
Shahzad Haider
parent
6f3c981e7b
commit
67d0241853
@@ -81,7 +81,13 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
s1.ExtraData["encrypted"] = "true"
|
||||
parsedKey, passphrase, err = Crack([]byte(token))
|
||||
if err != nil {
|
||||
// The key is encrypted and the passphrase could not be
|
||||
// recovered from the wordlist. It still represents a real
|
||||
// exposure (offline-crackable, weak/legacy KDFs, passphrase
|
||||
// often reused or committed nearby), so surface it as an
|
||||
// unverified finding instead of dropping it silently.
|
||||
s1.SetVerificationError(err, token)
|
||||
results = append(results, s1)
|
||||
continue
|
||||
}
|
||||
if passphrase != "" {
|
||||
|
||||
@@ -96,3 +96,44 @@ func TestPrivatekey_Pattern(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// encryptedUncrackablePattern is an ed25519 key protected by a passphrase that
|
||||
// is not present in the built-in wordlist (pkg/detectors/privatekey/list.txt),
|
||||
// so Crack cannot recover it.
|
||||
var encryptedUncrackablePattern = `-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDnvQyInG
|
||||
vB+yh/WYczTGXbAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAILUSv84lMSLVN0iP
|
||||
knFlHoYobo1oohtLNp/ihBaf76QxAAAAoErDjg2HQs1hgYm0vfyrpBxWrcJ1u/LQG4o6cm
|
||||
GJ3NnZmMmJemrnMXjGHB3zj63AEPxxyb6BWZH5Olb5gYMgWcnOU91cGvyC7aT6C5cOnFb1
|
||||
ZtkdxTxqeUOMFS51gwDT04aURSz/caCV9KN4Y2MCHyW+GWxxD7eL0R6KhyT2j0z2BewCw9
|
||||
kRN1fq3C2rTb+wTaNz8q9X0R+6JGxfXGhpTJ0=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
`
|
||||
|
||||
// TestPrivatekey_EncryptedKeyReported asserts that a passphrase-protected key
|
||||
// whose passphrase cannot be cracked is still surfaced as an unverified finding
|
||||
// rather than being dropped silently (issue #5115).
|
||||
func TestPrivatekey_EncryptedKeyReported(t *testing.T) {
|
||||
d := Scanner{}
|
||||
input := fmt.Sprintf("%s = '%s'", keyword, encryptedUncrackablePattern)
|
||||
|
||||
results, err := d.FromData(context.Background(), false, []byte(input))
|
||||
if err != nil {
|
||||
t.Fatalf("FromData error = %v", err)
|
||||
}
|
||||
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("expected 1 result for an encrypted key with an uncrackable passphrase, got %d", len(results))
|
||||
}
|
||||
|
||||
r := results[0]
|
||||
if r.Verified {
|
||||
t.Errorf("expected the finding to be unverified")
|
||||
}
|
||||
if r.VerificationError() == nil {
|
||||
t.Errorf("expected a verification error to be set on the finding")
|
||||
}
|
||||
if r.ExtraData["encrypted"] != "true" {
|
||||
t.Errorf("expected ExtraData[\"encrypted\"] = \"true\", got %q", r.ExtraData["encrypted"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user