[Feat] PrivateKey Analyzer (#3854)

* private key analyzer initial impl  - basic structure

* Impl analyzePermission method for PrivateKey analyzer.
export few private functions for reusability in analyzers

* add permissions.yaml and generate permissions

* permissions fix

* AnalyzeAndPrintPermissions impl

* analyzer test pushed

* Impl analyzer func called from detector.
added and regenerated permissions.

* filter empty strings in result and generate expected_output for test

* some refactoring

* comment added for better readability.

* fixed code breaking changes in detectors due to exporting private functions.

* log insufficient information message on no certificate results
This commit is contained in:
Abdul Basit
2025-02-04 09:50:57 -06:00
committed by GitHub
parent f11c9279ed
commit 69b6d018d5
14 changed files with 627 additions and 37 deletions
+2
View File
@@ -80,6 +80,7 @@ const (
AnalyzerTypeSquare
AnalyzerTypeStripe
AnalyzerTypeTwilio
AnalyzerTypePrivateKey
// Add new items here with AnalyzerType prefix
)
@@ -106,6 +107,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
AnalyzerTypeSquare: "Square",
AnalyzerTypeStripe: "Stripe",
AnalyzerTypeTwilio: "Twilio",
AnalyzerTypePrivateKey: "PrivateKey",
// Add new mappings here
}
@@ -0,0 +1 @@
{"AnalyzerType":21,"Bindings":[],"UnboundedResources":[{"Name":"*.gruponu3.com","FullyQualifiedName":"/*.gruponu3.com","Type":"certificate","Metadata":null,"Parent":null},{"Name":"techautm.in","FullyQualifiedName":"/techautm.in","Type":"certificate","Metadata":null,"Parent":null}],"Metadata":null}
@@ -0,0 +1,141 @@
// Code generated by go generate; DO NOT EDIT.
package privatekey
import "errors"
type Permission int
const (
Invalid Permission = iota
Digitalsignature Permission = iota
Nonrepudiation Permission = iota
Keyencipherment Permission = iota
Dataencipherment Permission = iota
Keyagreement Permission = iota
Certificatesigning Permission = iota
Crlsigning Permission = iota
Encipheronly Permission = iota
Decipheronly Permission = iota
Serverauth Permission = iota
Clientauth Permission = iota
Codesigning Permission = iota
Emailprotection Permission = iota
Timestamping Permission = iota
Ocspsigning Permission = iota
Clone Permission = iota
Push Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Digitalsignature: "DigitalSignature",
Nonrepudiation: "NonRepudiation",
Keyencipherment: "KeyEncipherment",
Dataencipherment: "DataEncipherment",
Keyagreement: "KeyAgreement",
Certificatesigning: "CertificateSigning",
Crlsigning: "CRLSigning",
Encipheronly: "EncipherOnly",
Decipheronly: "DecipherOnly",
Serverauth: "ServerAuth",
Clientauth: "ClientAuth",
Codesigning: "CodeSigning",
Emailprotection: "EmailProtection",
Timestamping: "TimeStamping",
Ocspsigning: "OCSPSigning",
Clone: "Clone",
Push: "Push",
}
StringToPermission = map[string]Permission{
"DigitalSignature": Digitalsignature,
"NonRepudiation": Nonrepudiation,
"KeyEncipherment": Keyencipherment,
"DataEncipherment": Dataencipherment,
"KeyAgreement": Keyagreement,
"CertificateSigning": Certificatesigning,
"CRLSigning": Crlsigning,
"EncipherOnly": Encipheronly,
"DecipherOnly": Decipheronly,
"ServerAuth": Serverauth,
"ClientAuth": Clientauth,
"CodeSigning": Codesigning,
"EmailProtection": Emailprotection,
"TimeStamping": Timestamping,
"OCSPSigning": Ocspsigning,
"Clone": Clone,
"Push": Push,
}
PermissionIDs = map[Permission]int{
Digitalsignature: 1,
Nonrepudiation: 2,
Keyencipherment: 3,
Dataencipherment: 4,
Keyagreement: 5,
Certificatesigning: 6,
Crlsigning: 7,
Encipheronly: 8,
Decipheronly: 9,
Serverauth: 10,
Clientauth: 11,
Codesigning: 12,
Emailprotection: 13,
Timestamping: 14,
Ocspsigning: 15,
Clone: 16,
Push: 17,
}
IdToPermission = map[int]Permission{
1: Digitalsignature,
2: Nonrepudiation,
3: Keyencipherment,
4: Dataencipherment,
5: Keyagreement,
6: Certificatesigning,
7: Crlsigning,
8: Encipheronly,
9: Decipheronly,
10: Serverauth,
11: Clientauth,
12: Codesigning,
13: Emailprotection,
14: Timestamping,
15: Ocspsigning,
16: Clone,
17: Push,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}
@@ -0,0 +1,23 @@
permissions:
# TLS:
# KeyUsuage: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3
# ExtendedKeyUsage: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12
- DigitalSignature
- NonRepudiation
- KeyEncipherment
- DataEncipherment
- KeyAgreement
- CertificateSigning
- CRLSigning
- EncipherOnly
- DecipherOnly
- ServerAuth
- ClientAuth
- CodeSigning
- EmailProtection
- TimeStamping
- OCSPSigning
# Github/Gitlab
- Clone
- Push
@@ -0,0 +1,303 @@
//go:generate generate_permissions permissions.yaml permissions.go privatekey
package privatekey
import (
"errors"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/privatekey"
"golang.org/x/crypto/ssh"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypePrivateKey }
func (a Analyzer) Analyze(ctx context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
// token will be already normalized by the time it reaches here
token, ok := credInfo["token"]
if !ok {
return nil, errors.New("token not found in credInfo")
}
info, err := AnalyzePermissions(ctx, a.Cfg, token)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
type SecretInfo struct {
TLSCertificateResult *privatekey.DriftwoodResult
GithubUsername *string
GitlabUsername *string
}
func AnalyzePermissions(ctx context.Context, cfg *config.Config, token string) (*SecretInfo, error) {
var (
wg sync.WaitGroup
parsedKey any
err error
analyzerErrors = privatekey.NewVerificationErrors(3)
info = &SecretInfo{}
)
parsedKey, err = ssh.ParseRawPrivateKey([]byte(token))
if err != nil && strings.Contains(err.Error(), "private key is passphrase protected") {
// key is password protected
parsedKey, _, err = privatekey.Crack([]byte(token))
if err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
fingerprint, err := privatekey.FingerprintPEMKey(parsedKey)
if err != nil {
return nil, err
}
// Look up certificate information.
wg.Add(1)
go func() {
defer wg.Done()
data, err := analyzeFingerprint(ctx, fingerprint)
if err != nil {
analyzerErrors.Add(err)
} else {
info.TLSCertificateResult = data
}
}()
// Test SSH key against github.com
wg.Add(1)
go func() {
defer wg.Done()
user, err := analyzeGithubUser(ctx, parsedKey)
if err != nil {
analyzerErrors.Add(err)
} else if user != nil {
info.GithubUsername = user
}
}()
// Test SSH key against gitlab.com
wg.Add(1)
go func() {
defer wg.Done()
user, err := analyzeGitlabUser(ctx, parsedKey)
if err != nil {
analyzerErrors.Add(err)
} else if user != nil {
info.GitlabUsername = user
}
}()
wg.Wait()
if len(analyzerErrors.Errors) == 3 {
return nil, fmt.Errorf("analyzer failures: %s", strings.Join(analyzerErrors.Errors, ", "))
}
return info, nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
token := privatekey.Normalize(key)
if len(token) < 64 {
color.Red("[x] Error: Invalid Private Key")
return
}
info, err := AnalyzePermissions(context.Background(), cfg, token)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
color.Green("[!] Valid Private Key\n\n")
if info.GithubUsername == nil && info.GitlabUsername == nil && info.TLSCertificateResult == nil {
color.Yellow("[i] Insufficient information returned from fingerprint analysis. No permissions found.")
return
}
if info.GithubUsername != nil {
color.Yellow("[i] GitHub Details:")
printUserInfo(*info.GithubUsername)
}
if info.GitlabUsername != nil {
color.Yellow("[i] GitLab Details:")
printUserInfo(*info.GitlabUsername)
}
if info.TLSCertificateResult != nil {
printTLSCertificateResult(info.TLSCertificateResult)
}
}
func printUserInfo(username string) {
color.Yellow("[i] Username: %s", username)
color.Yellow("[i] Permissions: %s\n\n", color.GreenString("Clone/Push"))
}
func printTLSCertificateResult(result *privatekey.DriftwoodResult) {
color.Yellow("[i] TLS Certificate Details:")
fmt.Print("\n")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(
table.Row{"Subject Key ID", "Subject Name", "Subject Organization", "Permissions", "Expiration Date", "Domains"})
green := color.New(color.FgGreen).SprintFunc()
for _, certificateResult := range result.CertificateResults {
t.AppendRow([]interface{}{
green(certificateResult.SubjectKeyID),
green(certificateResult.SubjectName),
green(strings.Join(certificateResult.SubjectOrganization, ", ")),
green(strings.Join(append(certificateResult.KeyUsages, certificateResult.ExtendedKeyUsages...), ", ")),
green(certificateResult.ExpirationTimestamp.Format(time.RFC3339)),
green(strings.Join(certificateResult.Domains, ", ")),
})
}
t.Render()
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypePrivateKey,
Metadata: nil,
Bindings: []analyzers.Binding{},
UnboundedResources: []analyzers.Resource{},
}
if info.TLSCertificateResult != nil {
bounded, unbounded := bakeTLSResources(info.TLSCertificateResult)
result.Bindings = append(result.Bindings, bounded...)
result.UnboundedResources = append(result.UnboundedResources, unbounded...)
}
if info.GithubUsername != nil {
result.Bindings = append(result.Bindings, bakeGithubResources(info.GithubUsername)...)
}
if info.GitlabUsername != nil {
result.Bindings = append(result.Bindings, bakeGitlabResources(info.GitlabUsername)...)
}
return &result
}
func bakeGithubResources(username *string) []analyzers.Binding {
resource := &analyzers.Resource{
Name: *username,
FullyQualifiedName: fmt.Sprintf("github.com/user/%s", *username),
Type: "user", // always user ???
}
permissions := []analyzers.Permission{
{Value: PermissionStrings[Clone], Parent: nil},
{Value: PermissionStrings[Push], Parent: nil},
}
return analyzers.BindAllPermissions(*resource, permissions...)
}
func bakeGitlabResources(username *string) []analyzers.Binding {
resource := &analyzers.Resource{
Name: *username,
FullyQualifiedName: fmt.Sprintf("gitlab.com/user/%s", *username),
Type: "user", // always user ???
}
permissions := []analyzers.Permission{
{Value: PermissionStrings[Clone], Parent: nil},
{Value: PermissionStrings[Push], Parent: nil},
}
return analyzers.BindAllPermissions(*resource, permissions...)
}
func bakeTLSResources(result *privatekey.DriftwoodResult) ([]analyzers.Binding, []analyzers.Resource) {
unboundedResources := make([]analyzers.Resource, 0, len(result.CertificateResults))
boundedResources := make([]analyzers.Binding, 0, len(result.CertificateResults))
// iterate result.CertificateResults
for _, cert := range result.CertificateResults {
if cert.SubjectName == "" && cert.SubjectKeyID == "" {
continue
}
resource := &analyzers.Resource{
Name: cert.SubjectName,
FullyQualifiedName: fmt.Sprintf("%s/%s", cert.SubjectKeyID, cert.SubjectName),
Type: "certificate",
}
certPermissions := append(cert.KeyUsages, cert.ExtendedKeyUsages...)
permissions := make([]analyzers.Permission, 0, len(certPermissions))
for _, perm := range certPermissions {
perm, ok := StringToPermission[perm]
if !ok {
continue
}
permissions = append(permissions, analyzers.Permission{
Value: PermissionStrings[perm],
Parent: nil,
})
}
if len(permissions) > 0 {
// bind all permissions with resources
boundedResources = append(boundedResources, analyzers.BindAllPermissions(*resource, permissions...)...)
} else {
unboundedResources = append(unboundedResources, *resource)
}
}
return boundedResources, unboundedResources
}
func analyzeFingerprint(ctx context.Context, fingerprint string) (*privatekey.DriftwoodResult, error) {
result, err := privatekey.LookupFingerprint(ctx, fingerprint)
if err != nil {
return nil, err
}
if len(result.CertificateResults) == 0 {
return nil, nil
}
return result, nil
}
func analyzeGithubUser(ctx context.Context, parsedKey any) (*string, error) {
return privatekey.VerifyGitHubUser(ctx, parsedKey)
}
func analyzeGitlabUser(ctx context.Context, parsedKey any) (*string, error) {
return privatekey.VerifyGitLabUser(ctx, parsedKey)
}
@@ -0,0 +1,86 @@
package privatekey
import (
_ "embed"
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
privateKey := testSecrets.MustGetField("PRIVATEKEY_TLS")
tests := []struct {
name string
key string
storeUrl string
want string
wantErr bool
}{
{
name: "valid TLS key",
key: privateKey,
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
+3
View File
@@ -19,6 +19,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/opsgenie"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postgres"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postman"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/privatekey"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/sendgrid"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/shopify"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/slack"
@@ -102,5 +103,7 @@ func Run(cmd string) {
shopify.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"], secretInfo.Parts["url"])
case "opsgenie":
opsgenie.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
case "privatekey":
privatekey.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ var (
ErrUncrackable = errors.New("unable to crack encryption")
)
func crack(in []byte) (any, string, error) {
func Crack(in []byte) (any, string, error) {
for _, passphrase := range passphrases {
parsed, err := ssh.ParseRawPrivateKeyWithPassphrase(in, passphrase)
if err != nil {
+3 -3
View File
@@ -13,7 +13,7 @@ import (
var (
testEncryptedKeyCorrectPassword = []byte("123456")
testEncryptedKeyIncorrectPassword = []byte("incorrect")
testEncryptedKey = []byte(normalize(`-----BEGIN OPENSSH PRIVATE KEY-----
testEncryptedKey = []byte(Normalize(`-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAjNIZuun
xgLkM8KuzfmQuRAAAAEAAAAAEAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQDe3Al0EMPz
utVNk5DixaYrGMK56RqUoqGBinke6SWVWmqom1lBcJWzor6HlnMRPPr7YCEsJKL4IpuVwu
@@ -70,7 +70,7 @@ func Test_crack(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, passphrase, err := crack(tt.in)
_, passphrase, err := Crack(tt.in)
if (err != nil) != tt.wantErr {
t.Errorf("crack() error = %v, wantErr %v", err, tt.wantErr)
return
@@ -97,6 +97,6 @@ func BenchmarkParseRightPassword(b *testing.B) {
func BenchmarkCracker(b *testing.B) {
for n := 0; n < b.N; n++ {
crack(testEncryptedKey)
Crack(testEncryptedKey)
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"strings"
)
func normalize(in string) string {
func Normalize(in string) string {
in = strings.ReplaceAll(in, `"`, "")
in = strings.ReplaceAll(in, `'`, "")
in = strings.ReplaceAll(in, "\t", "")
+58 -28
View File
@@ -50,7 +50,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
matches := keyPat.FindAllString(dataStr, -1)
for _, match := range matches {
token := normalize(match)
token := Normalize(match)
if len(token) < 64 {
continue
}
@@ -66,7 +66,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
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))
parsedKey, passphrase, err = Crack([]byte(token))
if err != nil {
s1.SetVerificationError(err, token)
continue
@@ -88,14 +88,14 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
var (
wg sync.WaitGroup
extraData = newExtraData()
verificationErrors = newVerificationErrors()
verificationErrors = NewVerificationErrors(3)
)
// Look up certificate information.
wg.Add(1)
go func() {
defer wg.Done()
data, err := lookupFingerprint(ctx, fingerprint, s.IncludeExpired)
data, err := lookupFingerprintCertificateUrls(ctx, fingerprint, s.IncludeExpired)
if err == nil {
if data != nil {
extraData.Add("certificate_urls", strings.Join(data.CertificateURLs, ", "))
@@ -109,7 +109,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
wg.Add(1)
go func() {
defer wg.Done()
user, err := verifyGitHubUser(ctx, parsedKey)
user, err := VerifyGitHubUser(ctx, parsedKey)
if err != nil && !errors.Is(err, errPermissionDenied) {
verificationErrors.Add(err)
}
@@ -122,7 +122,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
wg.Add(1)
go func() {
defer wg.Done()
user, err := verifyGitLabUser(ctx, parsedKey)
user, err := VerifyGitLabUser(ctx, parsedKey)
if err != nil && !errors.Is(err, errPermissionDenied) {
verificationErrors.Add(err)
}
@@ -137,11 +137,16 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
for k, v := range extraData.data {
s1.ExtraData[k] = v
}
// enabled th
s1.AnalysisInfo = map[string]string{
"token": token,
}
} else {
s1.ExtraData = nil
}
if len(verificationErrors.errors) > 0 {
s1.SetVerificationError(fmt.Errorf("verification failures: %s", strings.Join(verificationErrors.errors, ", ")), token)
if len(verificationErrors.Errors) > 0 {
s1.SetVerificationError(fmt.Errorf("verification failures: %s", strings.Join(verificationErrors.Errors, ", ")), token)
}
}
@@ -164,19 +169,15 @@ type result struct {
GitHubUsername string
}
func lookupFingerprint(ctx context.Context, publicKeyFingerprintInHex string, includeExpired bool) (*result, 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 res.Body.Close()
results := DriftwoodResult{}
err = json.NewDecoder(res.Body).Decode(&results)
func lookupFingerprintCertificateUrls(
ctx context.Context,
publicKeyFingerprintInHex string,
includeExpired bool,
) (*result, error) {
results, err := LookupFingerprint(
ctx,
publicKeyFingerprintInHex,
)
if err != nil {
return nil, err
}
@@ -201,10 +202,39 @@ func lookupFingerprint(ctx context.Context, publicKeyFingerprintInHex string, in
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 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"`
@@ -228,20 +258,20 @@ func (e *extraData) Add(key string, value string) {
e.mutex.Unlock()
}
type verificationErrors struct {
type VerificationErrors struct {
mutex sync.Mutex
errors []string
Errors []string
}
func newVerificationErrors() *verificationErrors {
return &verificationErrors{
errors: make([]string, 0, 3),
func NewVerificationErrors(capacity int) *VerificationErrors {
return &VerificationErrors{
Errors: make([]string, 0, capacity),
}
}
func (e *verificationErrors) Add(err error) {
func (e *VerificationErrors) Add(err error) {
e.mutex.Lock()
e.errors = append(e.errors, err.Error())
e.Errors = append(e.Errors, err.Error())
e.mutex.Unlock()
}
@@ -154,6 +154,7 @@ func TestPrivatekey_FromChunk(t *testing.T) {
t.Fatal("no raw secret present")
}
got[i].Raw = nil
got[i].AnalysisInfo = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("PrivatekeyCI.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
@@ -203,7 +204,7 @@ func Test_lookupFingerprint(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotFingerprints, err := lookupFingerprint(context.TODO(), tt.publicKeyFingerprintInHex, tt.includeExpired)
gotFingerprints, err := lookupFingerprintCertificateUrls(context.TODO(), tt.publicKeyFingerprintInHex, tt.includeExpired)
if (err != nil) != tt.wantErr {
t.Errorf("lookupFingerprint() error = %v, wantErr %v", err, tt.wantErr)
return
+2 -2
View File
@@ -103,7 +103,7 @@ func sshDialWithContext(ctx context.Context, network, addr string, config *ssh.C
var errPermissionDenied = errors.New("permission denied")
func verifyGitHubUser(ctx context.Context, parsedKey any) (*string, error) {
func VerifyGitHubUser(ctx context.Context, parsedKey any) (*string, error) {
output, err := firstResponseFromSSH(ctx, parsedKey, "git", "github.com:22")
if err != nil {
return nil, err
@@ -121,7 +121,7 @@ func verifyGitHubUser(ctx context.Context, parsedKey any) (*string, error) {
return nil, nil
}
func verifyGitLabUser(ctx context.Context, parsedKey any) (*string, error) {
func VerifyGitLabUser(ctx context.Context, parsedKey any) (*string, error) {
output, err := firstResponseFromSSH(ctx, parsedKey, "git", "gitlab.com:22")
if err != nil {
return nil, err
@@ -22,7 +22,7 @@ func TestFirstResponseFromSSH(t *testing.T) {
}
secretGitHub := testSecrets.MustGetField("PRIVATEKEY_GITHUB")
parsedKey, err := ssh.ParseRawPrivateKey([]byte(normalize(secretGitHub)))
parsedKey, err := ssh.ParseRawPrivateKey([]byte(Normalize(secretGitHub)))
if err != nil {
t.Fatalf("could not parse test secret: %s", err)
}