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]>
298 lines
8.3 KiB
Go
298 lines
8.3 KiB
Go
package privatekey
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
regexp "github.com/wasilibs/go-re2"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
|
)
|
|
|
|
var (
|
|
falsePositiveGHUsernames = map[detectors.FalsePositive]struct{}{
|
|
// This hack is because it's probably one of the most widely distributed github keys
|
|
// and a frequent annoyance.
|
|
// It is active at the time of this commit, but the developer is unresponsive.
|
|
detectors.FalsePositive("aaron1234567890123"): {},
|
|
}
|
|
)
|
|
|
|
type Scanner struct {
|
|
IncludeExpired bool
|
|
}
|
|
|
|
// Ensure the Scanner satisfies the interface at compile time.
|
|
var _ detectors.Detector = (*Scanner)(nil)
|
|
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
|
|
var _ detectors.MaxSecretSizeProvider = (*Scanner)(nil)
|
|
|
|
var (
|
|
// TODO: add base64 encoded key support
|
|
client = common.RetryableHTTPClient()
|
|
keyPat = regexp.MustCompile(`(?i)-----\s*?BEGIN[ A-Z0-9_-]*?PRIVATE KEY\s*?-----[\s\S]*?----\s*?END[ A-Z0-9_-]*? PRIVATE KEY\s*?-----`)
|
|
)
|
|
|
|
// Keywords are used for efficiently pre-filtering chunks.
|
|
// Use identifiers in the secret preferably, or the provider name.
|
|
func (s Scanner) Keywords() []string {
|
|
return []string{"private key"}
|
|
}
|
|
|
|
const maxPrivateKeySize = 4096
|
|
|
|
// ProvideMaxSecretSize returns the maximum size of a secret that this detector can find.
|
|
func (s Scanner) MaxSecretSize() int64 { return maxPrivateKeySize }
|
|
|
|
// FromData will find and optionally verify Privatekey secrets in a given set of bytes.
|
|
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
|
dataStr := string(data)
|
|
|
|
matches := keyPat.FindAllString(dataStr, -1)
|
|
for _, match := range matches {
|
|
token := Normalize(match)
|
|
if len(token) < 64 {
|
|
continue
|
|
}
|
|
|
|
s1 := detectors.Result{
|
|
DetectorType: detector_typepb.DetectorType_PrivateKey,
|
|
Raw: []byte(token),
|
|
Redacted: token[0:64],
|
|
ExtraData: make(map[string]string),
|
|
SecretParts: map[string]string{"token": token},
|
|
}
|
|
|
|
// set not normalized match as primary secret value so it is used to calculate line of code
|
|
s1.SetPrimarySecretValue(match)
|
|
|
|
var passphrase string
|
|
parsedKey, err := ssh.ParseRawPrivateKey([]byte(token))
|
|
if err != nil && strings.Contains(err.Error(), "private key is passphrase protected") {
|
|
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 != "" {
|
|
s1.ExtraData["cracked_encryption_passphrase"] = "true"
|
|
}
|
|
} else if err != nil {
|
|
// couldn't parse key, probably invalid
|
|
continue
|
|
}
|
|
|
|
fingerprint, err := FingerprintPEMKey(parsedKey)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if verify {
|
|
var (
|
|
wg sync.WaitGroup
|
|
extraData = newExtraData()
|
|
verificationErrors = NewVerificationErrors(3)
|
|
)
|
|
|
|
// Look up certificate information.
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
data, err := lookupFingerprintCertificateUrls(ctx, fingerprint, s.IncludeExpired)
|
|
if err == nil {
|
|
if data != nil {
|
|
extraData.Add("certificate_urls", strings.Join(data.CertificateURLs, ", "))
|
|
}
|
|
} else {
|
|
verificationErrors.Add(err)
|
|
}
|
|
}()
|
|
|
|
// Test SSH key against github.com
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
username, err := VerifyGitHubUser(ctx, parsedKey)
|
|
if err != nil && !errors.Is(err, errPermissionDenied) {
|
|
verificationErrors.Add(err)
|
|
}
|
|
if username != nil {
|
|
isFalsePositive, _ := detectors.IsKnownFalsePositive(*username, falsePositiveGHUsernames, false)
|
|
if !isFalsePositive {
|
|
extraData.Add("github_user", *username)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Test SSH key against gitlab.com
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
user, err := VerifyGitLabUser(ctx, parsedKey)
|
|
if err != nil && !errors.Is(err, errPermissionDenied) {
|
|
verificationErrors.Add(err)
|
|
}
|
|
if user != nil {
|
|
extraData.Add("gitlab_user", *user)
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
if len(extraData.data) > 0 {
|
|
s1.Verified = true
|
|
for k, v := range extraData.data {
|
|
s1.ExtraData[k] = v
|
|
}
|
|
} else {
|
|
s1.ExtraData = nil
|
|
}
|
|
if len(verificationErrors.Errors) > 0 {
|
|
s1.SetVerificationError(fmt.Errorf("verification failures: %s", strings.Join(verificationErrors.Errors, ", ")), token)
|
|
}
|
|
}
|
|
|
|
results = append(results, s1)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
|
|
return false, ""
|
|
}
|
|
|
|
func (s Scanner) Description() string {
|
|
return "Private keys are used for securely connecting and authenticating to various systems and services. Exposure of private keys can lead to unauthorized access and data breaches."
|
|
}
|
|
|
|
type result struct {
|
|
CertificateURLs []string
|
|
GitHubUsername string
|
|
}
|
|
|
|
func lookupFingerprintCertificateUrls(
|
|
ctx context.Context,
|
|
publicKeyFingerprintInHex string,
|
|
includeExpired bool,
|
|
) (*result, error) {
|
|
results, err := LookupFingerprint(
|
|
ctx,
|
|
publicKeyFingerprintInHex,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var data *result
|
|
|
|
seen := map[string]struct{}{}
|
|
for _, r := range results.CertificateResults {
|
|
if _, ok := seen[r.CertificateFingerprint]; ok {
|
|
continue
|
|
}
|
|
if !includeExpired && time.Since(r.ExpirationTimestamp) > 0 {
|
|
continue
|
|
}
|
|
if data == nil {
|
|
data = &result{}
|
|
}
|
|
data.CertificateURLs = append(data.CertificateURLs, fmt.Sprintf("https://crt.sh/?q=%s", r.CertificateFingerprint))
|
|
seen[r.CertificateFingerprint] = struct{}{}
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func LookupFingerprint(ctx context.Context, publicKeyFingerprintInHex string) (*DriftwoodResult, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://keychecker.trufflesecurity.com/fingerprint/%s", publicKeyFingerprintInHex), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = res.Body.Close() }()
|
|
|
|
results := DriftwoodResult{}
|
|
err = json.NewDecoder(res.Body).Decode(&results)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &results, nil
|
|
}
|
|
|
|
type DriftwoodResult struct {
|
|
CertificateResults []struct {
|
|
Domains []string `json:",omitempty"`
|
|
CertificateFingerprint string `json:"CertificateFingerprint"`
|
|
ExpirationTimestamp time.Time `json:"ExpirationTimestamp"`
|
|
IssuerName string `json:",omitempty"` // CA information
|
|
SubjectName string `json:",omitempty"` // Certificate subject
|
|
IssuerOrganization []string `json:",omitempty"` // CA organization(s)
|
|
SubjectOrganization []string `json:",omitempty"` // Subject organization(s)
|
|
KeyUsages []string `json:",omitempty"` // e.g., ["DigitalSignature", "KeyEncipherment"]
|
|
ExtendedKeyUsages []string `json:",omitempty"` // e.g., ["ServerAuth", "ClientAuth"]
|
|
SubjectKeyID string `json:",omitempty"` // hex encoded
|
|
AuthorityKeyID string `json:",omitempty"` // hex encoded
|
|
SerialNumber string `json:",omitempty"` // hex encoded
|
|
} `json:"CertificateResults"`
|
|
GitHubSSHResults []struct {
|
|
Username string `json:"Username"`
|
|
} `json:"GitHubSSHResults"`
|
|
}
|
|
|
|
type extraData struct {
|
|
mutex sync.Mutex
|
|
data map[string]string
|
|
}
|
|
|
|
func newExtraData() *extraData {
|
|
return &extraData{
|
|
data: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
func (e *extraData) Add(key string, value string) {
|
|
e.mutex.Lock()
|
|
e.data[key] = value
|
|
e.mutex.Unlock()
|
|
}
|
|
|
|
type VerificationErrors struct {
|
|
mutex sync.Mutex
|
|
Errors []string
|
|
}
|
|
|
|
func NewVerificationErrors(capacity int) *VerificationErrors {
|
|
return &VerificationErrors{
|
|
Errors: make([]string, 0, capacity),
|
|
}
|
|
}
|
|
|
|
func (e *VerificationErrors) Add(err error) {
|
|
e.mutex.Lock()
|
|
e.Errors = append(e.Errors, err.Error())
|
|
e.mutex.Unlock()
|
|
}
|
|
|
|
func (s Scanner) Type() detector_typepb.DetectorType {
|
|
return detector_typepb.DetectorType_PrivateKey
|
|
}
|