fix: resolve critical security vulnerabilities in debug logging and command execution

- Remove sensitive data from debug logs (vault/secrets.go, secret/version.go)
- Add input validation for GPG key IDs and keychain item names
- Resolve GPG key IDs to full fingerprints before storing in metadata
- Add comprehensive test coverage for validation functions
- Add golangci-lint configuration with additional linters

Security improvements:
- Debug logs no longer expose decrypted secret values or private keys
- GPG and keychain commands now validate input to prevent injection attacks
- All validation uses precompiled regex patterns for performance
This commit is contained in:
2025-06-20 07:50:26 -07:00
parent 004dce5472
commit 985d79d3c0
8 changed files with 529 additions and 28 deletions
+66 -2
View File
@@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
@@ -24,6 +25,19 @@ var (
// GPGDecryptFunc is the function used for GPG decryption
// Can be overridden in tests to provide a non-interactive implementation
GPGDecryptFunc = gpgDecryptDefault
// gpgKeyIDRegex validates GPG key IDs
// Allows either:
// 1. Email addresses (user@domain.tld format)
// 2. Short key IDs (8 hex characters)
// 3. Long key IDs (16 hex characters)
// 4. Full fingerprints (40 hex characters)
gpgKeyIDRegex = regexp.MustCompile(
`^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$|` +
`^[A-Fa-f0-9]{8}$|` +
`^[A-Fa-f0-9]{16}$|` +
`^[A-Fa-f0-9]{40}$`,
)
)
// PGPUnlockerMetadata extends UnlockerMetadata with PGP-specific data
@@ -285,14 +299,20 @@ func CreatePGPUnlocker(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnloc
return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
}
// Step 9: Create and write enhanced metadata
// Step 9: Resolve the GPG key ID to its full fingerprint
fingerprint, err := resolveGPGKeyFingerprint(gpgKeyID)
if err != nil {
return nil, fmt.Errorf("failed to resolve GPG key fingerprint: %w", err)
}
// Step 10: Create and write enhanced metadata with full fingerprint
pgpMetadata := PGPUnlockerMetadata{
UnlockerMetadata: UnlockerMetadata{
Type: "pgp",
CreatedAt: time.Now(),
Flags: []string{"gpg", "encrypted"},
},
GPGKeyID: gpgKeyID,
GPGKeyID: fingerprint,
}
metadataBytes, err := json.MarshalIndent(pgpMetadata, "", " ")
@@ -311,6 +331,46 @@ func CreatePGPUnlocker(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnloc
}, nil
}
// validateGPGKeyID validates that a GPG key ID is safe for command execution
func validateGPGKeyID(keyID string) error {
if keyID == "" {
return fmt.Errorf("GPG key ID cannot be empty")
}
if !gpgKeyIDRegex.MatchString(keyID) {
return fmt.Errorf("invalid GPG key ID format: %s", keyID)
}
return nil
}
// resolveGPGKeyFingerprint resolves any GPG key identifier to its full fingerprint
func resolveGPGKeyFingerprint(keyID string) (string, error) {
if err := validateGPGKeyID(keyID); err != nil {
return "", fmt.Errorf("invalid GPG key ID: %w", err)
}
// Use GPG to get the full fingerprint for the key
cmd := exec.Command("gpg", "--list-keys", "--with-colons", "--fingerprint", keyID)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to resolve GPG key fingerprint: %w", err)
}
// Parse the output to extract the fingerprint
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "fpr:") {
fields := strings.Split(line, ":")
if len(fields) >= 10 && fields[9] != "" {
return fields[9], nil
}
}
}
return "", fmt.Errorf("could not find fingerprint for GPG key: %s", keyID)
}
// checkGPGAvailable verifies that GPG is available
func checkGPGAvailable() error {
cmd := exec.Command("gpg", "--version")
@@ -322,6 +382,10 @@ func checkGPGAvailable() error {
// gpgEncryptDefault is the default implementation of GPG encryption
func gpgEncryptDefault(data []byte, keyID string) ([]byte, error) {
if err := validateGPGKeyID(keyID); err != nil {
return nil, fmt.Errorf("invalid GPG key ID: %w", err)
}
cmd := exec.Command("gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID)
cmd.Stdin = strings.NewReader(string(data))