mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
add support to merge system CA with PMG CA (#169)
* add support to merge system CA with PMG CA * Keep CA cert immutable; merge system bundle only when writing proxy CA file * Update proxy/certmanager/generator.go Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> * Update internal/flows/proxy_flow.go Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com> Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> * update config field name * Use integer-only guard when sizing merged CA bundle * Use int64-only guard when sizing merged CA bundle * add tests for firstReadablePath * Update proxy/certmanager/generator.go Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> * fix linter * refactor system CA merge API * add max limit to 2MB for certificates * update max limit to 2MB for certificate merging * remove python specific env var for CAs --------- Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
This commit is contained in:
co-authored by
devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Abhisek Datta
devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
b0305cb54d
commit
a81a4918c0
@@ -206,13 +206,13 @@ func handleExecutionResultError(err error) error {
|
||||
return fmt.Errorf("failed to execute command: %w", err)
|
||||
}
|
||||
|
||||
// setupCACertificate generates or loads a CA certificate for MITM
|
||||
// setupCACertificate generates CA for MITM and writes proxy bundle for child package managers.
|
||||
func (f *proxyFlow) setupCACertificate() (*certmanager.Certificate, string, error) {
|
||||
log.Debugf("Generating CA certificate for proxy MITM")
|
||||
|
||||
// Generate CA certificate
|
||||
caConfig := certmanager.DefaultCertManagerConfig()
|
||||
caCert, err := certmanager.GenerateCA(caConfig)
|
||||
caCert, err := certmanager.GenerateCAWithSystemCA(caConfig)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to generate CA certificate: %w", err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package certmanager
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -245,3 +248,101 @@ func BenchmarkCacheOperations(b *testing.B) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateCAWithSystemCA(t *testing.T) {
|
||||
config := DefaultCertManagerConfig()
|
||||
|
||||
systemBundlePath := filepath.Join(t.TempDir(), "system.pem")
|
||||
assert.NoError(t, os.WriteFile(systemBundlePath, []byte("SYSTEM_CA_CERT\n"), 0600))
|
||||
|
||||
t.Setenv("SSL_CERT_FILE", systemBundlePath)
|
||||
|
||||
ca, err := GenerateCAWithSystemCA(config)
|
||||
assert.NoError(t, err, "Failed to generate CA with system CA bundle")
|
||||
assert.NotNil(t, ca, "CA certificate should not be nil")
|
||||
assert.True(t, strings.Contains(string(ca.Certificate), "SYSTEM_CA_CERT"), "Merged certificate should contain system CA content")
|
||||
}
|
||||
|
||||
func TestSystemCABundleCandidatesForOS_WindowsIncludesCommonBundlePaths(t *testing.T) {
|
||||
t.Setenv("SSL_CERT_FILE", "")
|
||||
|
||||
t.Setenv("CURL_CA_BUNDLE", "")
|
||||
t.Setenv("ProgramFiles", `C:\Program Files`)
|
||||
t.Setenv("ProgramFiles(x86)", `C:\Program Files (x86)`)
|
||||
t.Setenv("SystemRoot", `C:\Windows`)
|
||||
|
||||
candidates := systemCABundleCandidatesForOS(goosWindows)
|
||||
joined := strings.Join(candidates, "|")
|
||||
|
||||
assert.Contains(t, joined, `Git/mingw64/ssl/certs/ca-bundle.crt`)
|
||||
assert.Contains(t, joined, `Git/usr/ssl/certs/ca-bundle.crt`)
|
||||
assert.Contains(t, joined, `Git/mingw32/ssl/certs/ca-bundle.crt`)
|
||||
assert.Contains(t, joined, `System32/curl-ca-bundle.crt`)
|
||||
}
|
||||
|
||||
func TestSystemCABundleCandidatesForOS_DarwinIncludesKnownBundlePaths(t *testing.T) {
|
||||
t.Setenv("SSL_CERT_FILE", "")
|
||||
|
||||
t.Setenv("CURL_CA_BUNDLE", "")
|
||||
candidates := systemCABundleCandidatesForOS(goosDarwin)
|
||||
joined := strings.Join(candidates, "|")
|
||||
|
||||
assert.Contains(t, joined, "/opt/homebrew/etc/openssl@3/cert.pem")
|
||||
assert.Contains(t, joined, "/usr/local/etc/openssl@3/cert.pem")
|
||||
assert.Contains(t, joined, "/etc/ssl/cert.pem")
|
||||
}
|
||||
|
||||
func TestSystemCABundleCandidatesForOS_LinuxIncludesKnownBundlePaths(t *testing.T) {
|
||||
t.Setenv("SSL_CERT_FILE", "")
|
||||
|
||||
t.Setenv("CURL_CA_BUNDLE", "")
|
||||
candidates := systemCABundleCandidatesForOS(goosLinux)
|
||||
joined := strings.Join(candidates, "|")
|
||||
|
||||
assert.Contains(t, joined, "/etc/ssl/certs/ca-certificates.crt")
|
||||
assert.Contains(t, joined, "/etc/pki/tls/certs/ca-bundle.crt")
|
||||
assert.Contains(t, joined, "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem")
|
||||
assert.Contains(t, joined, "/etc/ssl/ca-bundle.pem")
|
||||
assert.Contains(t, joined, "/etc/ssl/cert.pem")
|
||||
}
|
||||
|
||||
func TestFirstReadablePath(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
readableFile1 := filepath.Join(tempDir, "ca1.pem")
|
||||
readableFile2 := filepath.Join(tempDir, "ca2.pem")
|
||||
dirPath := filepath.Join(tempDir, "certs")
|
||||
missingFile := filepath.Join(tempDir, "missing.pem")
|
||||
|
||||
assert.NoError(t, os.MkdirAll(dirPath, 0o755))
|
||||
assert.NoError(t, os.WriteFile(readableFile1, []byte("CERT1"), 0o600))
|
||||
assert.NoError(t, os.WriteFile(readableFile2, []byte("CERT2"), 0o600))
|
||||
|
||||
t.Run("returns first readable file and skips empty, directory, and missing", func(t *testing.T) {
|
||||
got := firstReadablePath("", dirPath, missingFile, readableFile1, readableFile2)
|
||||
assert.Equal(t, readableFile1, got)
|
||||
})
|
||||
|
||||
t.Run("returns empty when no readable file exists", func(t *testing.T) {
|
||||
got := firstReadablePath("", dirPath, missingFile)
|
||||
assert.Equal(t, "", got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateCAWithSystemCA_SkipsOversizedSystemBundle(t *testing.T) {
|
||||
config := DefaultCertManagerConfig()
|
||||
|
||||
systemBundlePath := filepath.Join(t.TempDir(), "oversized-system.pem")
|
||||
oversized := make([]byte, maxSystemCABundleBytes+1)
|
||||
for i := range oversized {
|
||||
oversized[i] = 'A'
|
||||
}
|
||||
assert.NoError(t, os.WriteFile(systemBundlePath, oversized, 0600))
|
||||
|
||||
t.Setenv("SSL_CERT_FILE", systemBundlePath)
|
||||
|
||||
ca, err := GenerateCAWithSystemCA(config)
|
||||
assert.NoError(t, err, "oversized system CA bundle should be skipped")
|
||||
assert.NotNil(t, ca, "CA certificate should not be nil")
|
||||
assert.Less(t, len(ca.Certificate), int(maxSystemCABundleBytes), "oversized system CA content must not be merged")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,19 @@ import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSystemCABundleBytes int64 = 10 * 1024 * 1024
|
||||
goosDarwin = "darwin"
|
||||
goosLinux = "linux"
|
||||
goosWindows = "windows"
|
||||
)
|
||||
|
||||
// certManager implements the CertificateManager interface
|
||||
@@ -260,3 +272,158 @@ func ParseTLSCertificate(cert *Certificate) (tls.Certificate, error) {
|
||||
|
||||
return tlsCert, nil
|
||||
}
|
||||
|
||||
// GenerateCAWithSystemCA generates a self-signed certificate and appends system CA bundle
|
||||
// content to the certificate bytes. If the system bundle is unavailable or too large to merge,
|
||||
// it falls back to the PMG CA so proxy startup remains functional.
|
||||
func GenerateCAWithSystemCA(config CertManagerConfig) (*Certificate, error) {
|
||||
caCert, err := GenerateCA(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate CA: %w", err)
|
||||
}
|
||||
|
||||
caCertPEM := caCert.Certificate
|
||||
systemBundlePath := firstReadablePath(systemCABundleCandidates()...)
|
||||
|
||||
// No system CA found. Continue using only PMG cert.
|
||||
if systemBundlePath == "" {
|
||||
log.Warnf("Skipping system CA bundle merge: No system CA bundle file found")
|
||||
return caCert, nil
|
||||
}
|
||||
|
||||
info, err := os.Stat(systemBundlePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to stat system CA bundle %s: %w", systemBundlePath, err)
|
||||
}
|
||||
|
||||
// We make sure there is a boundary on the size of CA bundle loaded
|
||||
// from the system. Beyond that, we just skip it and return PMG cert.
|
||||
if info.Size() > maxSystemCABundleBytes {
|
||||
log.Errorf(
|
||||
"Skipping system CA bundle merge: %s is too large (%d bytes > %d bytes)",
|
||||
systemBundlePath,
|
||||
info.Size(),
|
||||
maxSystemCABundleBytes,
|
||||
)
|
||||
|
||||
return caCert, nil
|
||||
}
|
||||
|
||||
systemBundle, err := os.ReadFile(systemBundlePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read system CA bundle %s: %w", systemBundlePath, err)
|
||||
}
|
||||
|
||||
caLen := int64(len(caCertPEM))
|
||||
sysLen := int64(len(systemBundle))
|
||||
const extra = int64(2)
|
||||
|
||||
totalCap := caLen + sysLen + extra
|
||||
if totalCap > maxSystemCABundleBytes {
|
||||
log.Errorf(
|
||||
"Skipping system CA bundle merge: merged CA would be too large (%d bytes > %d bytes)",
|
||||
totalCap,
|
||||
maxSystemCABundleBytes,
|
||||
)
|
||||
|
||||
return caCert, nil
|
||||
}
|
||||
|
||||
merged := make([]byte, 0, int(totalCap))
|
||||
merged = append(merged, caCertPEM...)
|
||||
|
||||
if len(merged) > 0 && merged[len(merged)-1] != '\n' {
|
||||
merged = append(merged, '\n')
|
||||
}
|
||||
merged = append(merged, systemBundle...)
|
||||
|
||||
if len(merged) > 0 && merged[len(merged)-1] != '\n' {
|
||||
merged = append(merged, '\n')
|
||||
}
|
||||
|
||||
return &Certificate{
|
||||
Certificate: merged,
|
||||
PrivateKey: caCert.PrivateKey,
|
||||
X509Cert: caCert.X509Cert,
|
||||
PrivKey: caCert.PrivKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func firstReadablePath(paths ...string) string {
|
||||
for _, path := range paths {
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func systemCABundleCandidates() []string {
|
||||
return systemCABundleCandidatesForOS(runtime.GOOS)
|
||||
}
|
||||
|
||||
func systemCABundleCandidatesForOS(goos string) []string {
|
||||
var candidates []string
|
||||
appendIfSet := func(key string) {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
candidates = append(candidates, value)
|
||||
}
|
||||
}
|
||||
|
||||
appendIfSet("SSL_CERT_FILE")
|
||||
appendIfSet("CURL_CA_BUNDLE")
|
||||
|
||||
switch goos {
|
||||
case goosDarwin:
|
||||
candidates = append(candidates,
|
||||
"/opt/homebrew/etc/openssl@3/cert.pem",
|
||||
"/usr/local/etc/openssl@3/cert.pem",
|
||||
"/etc/ssl/cert.pem",
|
||||
)
|
||||
case goosLinux:
|
||||
candidates = append(candidates,
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
||||
"/etc/ssl/ca-bundle.pem",
|
||||
"/etc/ssl/cert.pem",
|
||||
)
|
||||
case goosWindows:
|
||||
programFiles := os.Getenv("ProgramFiles")
|
||||
programFilesX86 := os.Getenv("ProgramFiles(x86)")
|
||||
systemRoot := os.Getenv("SystemRoot")
|
||||
|
||||
if programFiles != "" {
|
||||
candidates = append(candidates,
|
||||
filepath.Join(programFiles, "Git", "mingw64", "ssl", "certs", "ca-bundle.crt"),
|
||||
filepath.Join(programFiles, "Git", "usr", "ssl", "certs", "ca-bundle.crt"),
|
||||
)
|
||||
}
|
||||
if programFilesX86 != "" {
|
||||
candidates = append(candidates,
|
||||
filepath.Join(programFilesX86, "Git", "mingw32", "ssl", "certs", "ca-bundle.crt"),
|
||||
)
|
||||
}
|
||||
if systemRoot != "" {
|
||||
candidates = append(candidates,
|
||||
filepath.Join(systemRoot, "System32", "curl-ca-bundle.crt"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user