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)
|
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) {
|
func (f *proxyFlow) setupCACertificate() (*certmanager.Certificate, string, error) {
|
||||||
log.Debugf("Generating CA certificate for proxy MITM")
|
log.Debugf("Generating CA certificate for proxy MITM")
|
||||||
|
|
||||||
// Generate CA certificate
|
// Generate CA certificate
|
||||||
caConfig := certmanager.DefaultCertManagerConfig()
|
caConfig := certmanager.DefaultCertManagerConfig()
|
||||||
caCert, err := certmanager.GenerateCA(caConfig)
|
caCert, err := certmanager.GenerateCAWithSystemCA(caConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to generate CA certificate: %w", err)
|
return nil, "", fmt.Errorf("failed to generate CA certificate: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package certmanager
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/safedep/dry/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxSystemCABundleBytes int64 = 10 * 1024 * 1024
|
||||||
|
goosDarwin = "darwin"
|
||||||
|
goosLinux = "linux"
|
||||||
|
goosWindows = "windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
// certManager implements the CertificateManager interface
|
// certManager implements the CertificateManager interface
|
||||||
@@ -260,3 +272,158 @@ func ParseTLSCertificate(cert *Certificate) (tls.Certificate, error) {
|
|||||||
|
|
||||||
return tlsCert, nil
|
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