mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: Add opt-in support for proxy CA cert installation (#318)
* feat: Add opt-in support for proxy CA cert installation * fix: Code review fixes * fix: Code review fixes * fix: Code review fixes * fix: Code review fixes * fix: Code review fixes * docs: Add limitation for MacOS MDM script
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/dry/usefulerror"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/errcodes"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"github.com/safedep/pmg/proxy/certmanager"
|
||||
"github.com/safedep/pmg/truststore"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// trustStore is the testability seam; defaultTrustStore delegates to the package.
|
||||
type trustStore interface {
|
||||
Install(certPEM []byte, scope truststore.Scope) error
|
||||
Uninstall(commonName string, scope truststore.Scope) error
|
||||
Status(commonName string) (user, system bool, err error)
|
||||
UserScopeSupported() bool
|
||||
}
|
||||
|
||||
type defaultTrustStore struct{}
|
||||
|
||||
func (defaultTrustStore) Install(p []byte, s truststore.Scope) error { return truststore.Install(p, s) }
|
||||
func (defaultTrustStore) Uninstall(cn string, s truststore.Scope) error {
|
||||
return truststore.Uninstall(cn, s)
|
||||
}
|
||||
func (defaultTrustStore) Status(cn string) (bool, bool, error) { return truststore.Status(cn) }
|
||||
func (defaultTrustStore) UserScopeSupported() bool { return truststore.UserScopeSupported() }
|
||||
|
||||
type certCommandError struct{ usefulerror.UsefulError }
|
||||
|
||||
func (e *certCommandError) ExitCode() int { return 1 }
|
||||
|
||||
func newCertCommandError(code, msg, help string, cause error) *certCommandError {
|
||||
return &certCommandError{
|
||||
UsefulError: usefulerror.NewUsefulError().
|
||||
WithCode(code).
|
||||
WithHumanError(msg).
|
||||
WithHelp(help).
|
||||
Wrap(cause),
|
||||
}
|
||||
}
|
||||
|
||||
// NewCertCommand returns the `pmg setup cert` command tree.
|
||||
func NewCertCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cert",
|
||||
Short: "Manage PMG's MITM CA certificate and OS trust",
|
||||
Long: "Generate, persist, and trust PMG's MITM CA so package managers and " +
|
||||
"native tools (including Go on macOS/Windows) trust HTTPS interception.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() },
|
||||
}
|
||||
cmd.AddCommand(newCertInstallCommand())
|
||||
cmd.AddCommand(newCertUninstallCommand())
|
||||
cmd.AddCommand(newCertStatusCommand())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func scopeFromFlag(system bool) truststore.Scope {
|
||||
if system {
|
||||
return truststore.ScopeSystem
|
||||
}
|
||||
return truststore.ScopeUser
|
||||
}
|
||||
|
||||
func newCertInstallCommand() *cobra.Command {
|
||||
var system, force bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Generate, persist, and trust PMG's MITM CA",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := errIfRunningUnderSudo(); err != nil {
|
||||
return err
|
||||
}
|
||||
return runCertInstall(config.Get().ConfigDir(), scopeFromFlag(system), force, defaultTrustStore{}, os.Stdout)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&system, "system", false, "Install into the system (all-users) trust store (PMG prompts for elevation; on Windows run from an elevated prompt)")
|
||||
cmd.Flags().BoolVar(&force, "force", false, "Regenerate and re-trust the CA even if one already exists")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCertUninstallCommand() *cobra.Command {
|
||||
var system, purge bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "Remove PMG's MITM CA from the OS trust store",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := errIfRunningUnderSudo(); err != nil {
|
||||
return err
|
||||
}
|
||||
return runCertUninstall(config.Get().ConfigDir(), scopeFromFlag(system), purge, defaultTrustStore{}, os.Stdout)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&system, "system", false, "Remove from the system (all-users) trust store (PMG prompts for elevation; on Windows run from an elevated prompt)")
|
||||
cmd.Flags().BoolVar(&purge, "purge", false, "Also delete the on-disk CA keypair")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCertStatusCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show PMG MITM CA presence, trust scope, and expiry",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := errIfRunningUnderSudo(); err != nil {
|
||||
return err
|
||||
}
|
||||
return runCertStatus(config.Get().ConfigDir(), defaultTrustStore{}, os.Stdout)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runCertInstall(dir string, scope truststore.Scope, force bool, store trustStore, out io.Writer) error {
|
||||
caCert, loadErr := certmanager.LoadCA(dir)
|
||||
exists := loadErr == nil
|
||||
// A failed load with files still on disk means a corrupt or partial CA (e.g.
|
||||
// the cert is present/trusted but ca-key.pem is missing or unparseable).
|
||||
// Treat it like a rotation so the old trusted root is cleaned up rather than
|
||||
// left behind alongside a freshly generated one. (A missing key surfaces as
|
||||
// os.ErrNotExist from LoadCA, so checking on-disk remnants is what tells a
|
||||
// partial state apart from a truly fresh install.)
|
||||
diskState, inspectErr := certmanager.InspectCA(dir)
|
||||
if inspectErr != nil {
|
||||
log.Debugf("inspecting on-disk CA during install: %v", inspectErr)
|
||||
}
|
||||
corrupted := loadErr != nil && (diskState.KeyPresent || diskState.CertPresent)
|
||||
expired := exists && caCert.IsExpired(time.Hour)
|
||||
rotate := force || expired || corrupted
|
||||
if corrupted {
|
||||
log.Debugf("replacing unreadable persisted CA: %v", loadErr)
|
||||
}
|
||||
|
||||
if exists && !rotate {
|
||||
user, system, _ := store.Status(certmanager.CACommonName)
|
||||
if (scope == truststore.ScopeUser && user) || (scope == truststore.ScopeSystem && system) {
|
||||
if _, err := fmt.Fprintf(out, "%s CA already installed and trusted (%s scope)\n", ui.Colors.Green("✓"), scope.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if !exists || rotate {
|
||||
if rotate && (exists || corrupted) {
|
||||
msg := "Rotating existing CA"
|
||||
if corrupted {
|
||||
msg = "Persisted CA is incomplete or unreadable; replacing it"
|
||||
}
|
||||
if _, err := fmt.Fprintf(out, "%s %s\n", ui.Colors.Dim("ℹ"), msg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.Uninstall(certmanager.CACommonName, scope); err != nil && !errors.Is(err, truststore.ErrUserScopeUnsupported) {
|
||||
return newCertCommandError(errcodes.CertTrustStore, "failed to remove old CA before rotation", trustHelp(scope), err)
|
||||
}
|
||||
|
||||
// Best-effort cleanup of the opposite scope so a scope change during
|
||||
// rotation leaves no stale same-CN cert behind. Non-fatal: the other
|
||||
// scope may need privileges we do not hold.
|
||||
if err := store.Uninstall(certmanager.CACommonName, otherScope(scope)); err != nil && !errors.Is(err, truststore.ErrUserScopeUnsupported) {
|
||||
log.Debugf("best-effort cleanup of %s-scope CA during rotation failed: %v", otherScope(scope), err)
|
||||
}
|
||||
}
|
||||
|
||||
generated, err := certmanager.GenerateCA(certmanager.PersistentCACertManagerConfig())
|
||||
if err != nil {
|
||||
return newCertCommandError(errcodes.CertGeneration, "failed to generate CA certificate",
|
||||
"Check available entropy and try again", err)
|
||||
}
|
||||
if err := certmanager.SaveCA(dir, generated); err != nil {
|
||||
return newCertCommandError(errcodes.CertPersistence, "failed to persist CA keypair",
|
||||
fmt.Sprintf("Check write permissions for %s", dir), err)
|
||||
}
|
||||
caCert = generated
|
||||
if _, err := fmt.Fprintf(out, "%s CA keypair written to %s\n", ui.Colors.Green("✓"), certmanager.CACertPath(dir)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := fmt.Fprintf(out, "%s Reusing existing CA keypair at %s\n", ui.Colors.Dim("ℹ"), certmanager.CACertPath(dir)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(out, "%s Installing an OS-trusted MITM CA (%s scope). This lets PMG inspect HTTPS package traffic.\n",
|
||||
ui.Colors.Yellow("⚠"), scope.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.Install(caCert.Certificate, scope); err != nil {
|
||||
if errors.Is(err, truststore.ErrUserScopeUnsupported) {
|
||||
// Linux has no per-user trust store; treat as a friendly no-op.
|
||||
if _, err := fmt.Fprintf(out, "%s %s\n", ui.Colors.Dim("ℹ"),
|
||||
"This platform has no per-user trust store. The CA keypair is persisted. "+
|
||||
"Re-run with --system for machine-wide trust (PMG prompts for elevation)."); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return newCertCommandError(errcodes.CertTrustStore, "failed to install CA into trust store", trustHelp(scope), err)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(out, "%s CA installed and trusted (%s scope)\n", ui.Colors.Green("✓"), scope.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCertUninstall(dir string, scope truststore.Scope, purge bool, store trustStore, out io.Writer) error {
|
||||
switch err := store.Uninstall(certmanager.CACommonName, scope); {
|
||||
case errors.Is(err, truststore.ErrUserScopeUnsupported):
|
||||
if _, e := fmt.Fprintf(out, "%s This platform has no per-user trust store; nothing to remove. Use --system for machine-wide.\n", ui.Colors.Dim("ℹ")); e != nil {
|
||||
return e
|
||||
}
|
||||
case err != nil:
|
||||
return newCertCommandError(errcodes.CertTrustStore, "failed to remove CA from trust store", trustHelp(scope), err)
|
||||
default:
|
||||
if _, e := fmt.Fprintf(out, "%s CA removed from %s trust store\n", ui.Colors.Green("✓"), scope.String()); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
if purge {
|
||||
removed := false
|
||||
for _, p := range []string{certmanager.CACertPath(dir), certmanager.CAKeyPath(dir)} {
|
||||
if err := os.Remove(p); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return newCertCommandError(errcodes.CertPersistence, "failed to delete CA file",
|
||||
"Check filesystem permissions", err)
|
||||
}
|
||||
removed = true
|
||||
}
|
||||
if removed {
|
||||
if _, err := fmt.Fprintf(out, "%s CA keypair deleted from disk\n", ui.Colors.Green("✓")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCertStatus(dir string, store trustStore, out io.Writer) error {
|
||||
st, err := certmanager.InspectCA(dir)
|
||||
if err != nil {
|
||||
return newCertCommandError(errcodes.CertPersistence, "failed to inspect CA",
|
||||
"The CA file may be corrupt; re-run `pmg setup cert install`", err)
|
||||
}
|
||||
|
||||
user, system, _ := store.Status(certmanager.CACommonName)
|
||||
st.UserTrusted, st.SystemTrusted = user, system
|
||||
|
||||
entries := map[string]string{
|
||||
"Key Present": strconv.FormatBool(st.KeyPresent),
|
||||
"Cert Present": strconv.FormatBool(st.CertPresent),
|
||||
"Trusted (user)": strconv.FormatBool(st.UserTrusted),
|
||||
"Trusted (system)": strconv.FormatBool(st.SystemTrusted),
|
||||
}
|
||||
if st.CertPresent {
|
||||
entries["Expires"] = st.NotAfter.Format(time.RFC3339)
|
||||
entries["Fingerprint"] = st.Fingerprint
|
||||
}
|
||||
ui.PrintInfoSection("PMG CA Certificate", entries)
|
||||
|
||||
drift, reason := st.Drift()
|
||||
switch {
|
||||
case drift:
|
||||
if _, err := fmt.Fprintf(out, "\n%s %s\n", ui.Colors.Red("drift:"), reason); err != nil {
|
||||
return err
|
||||
}
|
||||
case st.ExpiringSoon:
|
||||
if _, err := fmt.Fprintf(out, "\n%s CA expires within 30 days (%s). Run `pmg setup cert install --force` to rotate.\n",
|
||||
ui.Colors.Yellow("⚠"), st.NotAfter.Format("2006-01-02")); err != nil {
|
||||
return err
|
||||
}
|
||||
case st.KeyPresent && st.CertPresent && !st.Trusted():
|
||||
// "not trusted" is expected on Linux (Go honors SSL_CERT_FILE) but a
|
||||
// real problem where a per-user store exists (macOS/Windows).
|
||||
if store.UserScopeSupported() {
|
||||
if _, err := fmt.Fprintf(out, "\n%s CA on disk but not trusted in the OS store. Run `pmg setup cert install`.\n", ui.Colors.Yellow("⚠")); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := fmt.Fprintf(out, "\n%s CA on disk; not in OS store. Expected on Linux (Go honors SSL_CERT_FILE); use --system for store trust.\n", ui.Colors.Dim("ℹ")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func otherScope(s truststore.Scope) truststore.Scope {
|
||||
if s == truststore.ScopeSystem {
|
||||
return truststore.ScopeUser
|
||||
}
|
||||
return truststore.ScopeSystem
|
||||
}
|
||||
|
||||
var geteuid = os.Geteuid
|
||||
|
||||
func errIfRunningUnderSudo() error {
|
||||
if geteuid() == 0 && os.Getenv("SUDO_USER") != "" {
|
||||
return newCertCommandError(errcodes.PermissionDenied,
|
||||
"run `pmg setup cert` as your normal user, not with sudo",
|
||||
"PMG generates a per-user CA keypair and elevates only the system trust step. Re-run without sudo (use --system for machine-wide trust).",
|
||||
errors.New("invoked under sudo"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func trustHelp(scope truststore.Scope) string {
|
||||
if scope == truststore.ScopeSystem {
|
||||
return "Approve the elevation prompt when asked (macOS/Linux), or run from an elevated prompt (Windows)"
|
||||
}
|
||||
return "Approve the keychain prompt if shown; on Linux use --system (no per-user store)"
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/safedep/pmg/proxy/certmanager"
|
||||
"github.com/safedep/pmg/truststore"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeStore struct {
|
||||
installed bool
|
||||
installErr error
|
||||
uninstalled bool
|
||||
user, system bool
|
||||
userSupported bool
|
||||
}
|
||||
|
||||
func (f *fakeStore) Install(_ []byte, _ truststore.Scope) error {
|
||||
if f.installErr != nil {
|
||||
return f.installErr
|
||||
}
|
||||
f.installed = true
|
||||
return nil
|
||||
}
|
||||
func (f *fakeStore) Uninstall(_ string, _ truststore.Scope) error { f.uninstalled = true; return nil }
|
||||
func (f *fakeStore) Status(_ string) (bool, bool, error) { return f.user, f.system, nil }
|
||||
func (f *fakeStore) UserScopeSupported() bool { return f.userSupported }
|
||||
|
||||
func TestCertInstallGeneratesSavesAndInstalls(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store := &fakeStore{userSupported: true}
|
||||
var out bytes.Buffer
|
||||
|
||||
require.NoError(t, runCertInstall(dir, truststore.ScopeUser, false, store, &out))
|
||||
|
||||
assert.True(t, store.installed)
|
||||
_, err := certmanager.LoadCA(dir)
|
||||
require.NoError(t, err) // keypair persisted
|
||||
}
|
||||
|
||||
func TestCertInstallIdempotentWhenAlreadyTrusted(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ca, err := certmanager.GenerateCA(certmanager.PersistentCACertManagerConfig())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, certmanager.SaveCA(dir, ca))
|
||||
|
||||
store := &fakeStore{user: true, userSupported: true}
|
||||
var out bytes.Buffer
|
||||
require.NoError(t, runCertInstall(dir, truststore.ScopeUser, false, store, &out))
|
||||
|
||||
assert.False(t, store.installed) // already trusted → no re-install
|
||||
assert.Contains(t, out.String(), "already installed")
|
||||
}
|
||||
|
||||
func TestCertInstallLinuxUserNoopIsFriendly(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store := &fakeStore{installErr: truststore.ErrUserScopeUnsupported, userSupported: false}
|
||||
var out bytes.Buffer
|
||||
|
||||
require.NoError(t, runCertInstall(dir, truststore.ScopeUser, false, store, &out))
|
||||
assert.Contains(t, out.String(), "--system")
|
||||
}
|
||||
|
||||
func TestCertInstallForceRotates(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ca, err := certmanager.GenerateCA(certmanager.PersistentCACertManagerConfig())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, certmanager.SaveCA(dir, ca))
|
||||
|
||||
store := &fakeStore{userSupported: true}
|
||||
var out bytes.Buffer
|
||||
require.NoError(t, runCertInstall(dir, truststore.ScopeUser, true, store, &out))
|
||||
|
||||
assert.True(t, store.uninstalled) // rotation uninstalls old first
|
||||
assert.True(t, store.installed)
|
||||
}
|
||||
|
||||
func TestErrIfRunningUnderSudo(t *testing.T) {
|
||||
orig := geteuid
|
||||
t.Cleanup(func() { geteuid = orig })
|
||||
|
||||
// root + SUDO_USER set → refused (sudo from a normal user).
|
||||
geteuid = func() int { return 0 }
|
||||
t.Setenv("SUDO_USER", "alice")
|
||||
assert.Error(t, errIfRunningUnderSudo())
|
||||
|
||||
// root without SUDO_USER → genuine root, allowed.
|
||||
t.Setenv("SUDO_USER", "")
|
||||
assert.NoError(t, errIfRunningUnderSudo())
|
||||
|
||||
// non-root → allowed even if SUDO_USER somehow set.
|
||||
geteuid = func() int { return 1000 }
|
||||
t.Setenv("SUDO_USER", "alice")
|
||||
assert.NoError(t, errIfRunningUnderSudo())
|
||||
}
|
||||
|
||||
func TestCertInstallReplacesCorruptedCA(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ca, err := certmanager.GenerateCA(certmanager.PersistentCACertManagerConfig())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, certmanager.SaveCA(dir, ca))
|
||||
|
||||
// Simulate a partial/corrupt state: cert still on disk (and possibly trusted)
|
||||
// but the private key is gone. Install must clean up the old root, not stack a
|
||||
// second one alongside a freshly generated keypair.
|
||||
require.NoError(t, os.Remove(certmanager.CAKeyPath(dir)))
|
||||
|
||||
store := &fakeStore{userSupported: true}
|
||||
var out bytes.Buffer
|
||||
require.NoError(t, runCertInstall(dir, truststore.ScopeUser, false, store, &out))
|
||||
|
||||
assert.True(t, store.uninstalled, "old trusted root should be cleaned up")
|
||||
assert.True(t, store.installed)
|
||||
_, err = certmanager.LoadCA(dir)
|
||||
require.NoError(t, err) // a complete keypair is regenerated
|
||||
}
|
||||
|
||||
func TestCertUninstallPurgeDeletesFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ca, err := certmanager.GenerateCA(certmanager.PersistentCACertManagerConfig())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, certmanager.SaveCA(dir, ca))
|
||||
|
||||
store := &fakeStore{}
|
||||
var out bytes.Buffer
|
||||
require.NoError(t, runCertUninstall(dir, truststore.ScopeUser, true, store, &out))
|
||||
|
||||
assert.True(t, store.uninstalled)
|
||||
_, err = certmanager.LoadCA(dir)
|
||||
assert.Error(t, err) // files gone
|
||||
assert.NoFileExists(t, certmanager.CAKeyPath(dir))
|
||||
}
|
||||
|
||||
func TestCertStatusReportsDrift(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ca, err := certmanager.GenerateCA(certmanager.PersistentCACertManagerConfig())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, certmanager.SaveCA(dir, ca))
|
||||
// Simulate drift: remove the key, keep the cert.
|
||||
require.NoError(t, os.Remove(certmanager.CAKeyPath(dir)))
|
||||
|
||||
store := &fakeStore{}
|
||||
var out bytes.Buffer
|
||||
require.NoError(t, runCertStatus(dir, store, &out))
|
||||
assert.Contains(t, out.String(), "drift")
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import (
|
||||
"github.com/safedep/pmg/internal/shim"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"github.com/safedep/pmg/internal/version"
|
||||
"github.com/safedep/pmg/proxy/certmanager"
|
||||
"github.com/safedep/pmg/sandbox/platform"
|
||||
"github.com/safedep/pmg/truststore"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -28,6 +30,7 @@ const (
|
||||
checkSandbox = "sandbox"
|
||||
checkProtectionNpm = "protection-npm"
|
||||
checkProtectionPip = "protection-pip"
|
||||
checkCA = "ca-cert"
|
||||
)
|
||||
|
||||
func NewDoctorCommand() *cobra.Command {
|
||||
@@ -260,6 +263,14 @@ func runCoreChecks(cfg *config.RuntimeConfig) []doctor.CheckResult {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: checkCA,
|
||||
Category: "Security",
|
||||
Run: func() doctor.CheckResult {
|
||||
user, system, _ := truststore.Status(certmanager.CACommonName)
|
||||
return evaluateCACheck(cfg.ConfigDir(), user, system, truststore.UserScopeSupported())
|
||||
},
|
||||
},
|
||||
}
|
||||
return doctor.RunChecks(checks)
|
||||
}
|
||||
@@ -317,6 +328,7 @@ var checkDisplayNames = map[string]string{
|
||||
checkSandbox: "Sandbox",
|
||||
checkProtectionNpm: "npm protection",
|
||||
checkProtectionPip: "pip protection",
|
||||
checkCA: "MITM CA",
|
||||
}
|
||||
|
||||
var checkFixes = map[string]string{
|
||||
@@ -331,6 +343,41 @@ var checkFixes = map[string]string{
|
||||
checkEventLogging: "Set skip_event_logging: false in config",
|
||||
checkProtectionNpm: "pmg setup install",
|
||||
checkProtectionPip: "pmg setup install",
|
||||
checkCA: "pmg setup cert install",
|
||||
}
|
||||
|
||||
// evaluateCACheck is the testable core of the CA doctor check. Trust booleans
|
||||
// and userScopeSupported are injected so the check is hermetic; the live check
|
||||
// fills them from truststore.
|
||||
func evaluateCACheck(dir string, userTrusted, systemTrusted, userScopeSupported bool) doctor.CheckResult {
|
||||
st, err := certmanager.InspectCA(dir)
|
||||
if err != nil {
|
||||
return doctor.CheckResult{Status: doctor.StatusFail, Message: "CA files present but unreadable"}
|
||||
}
|
||||
|
||||
if !st.KeyPresent && !st.CertPresent {
|
||||
return doctor.CheckResult{Status: doctor.StatusWarn, Message: "MITM CA not installed (optional; run pmg setup cert install)"}
|
||||
}
|
||||
|
||||
st.UserTrusted, st.SystemTrusted = userTrusted, systemTrusted
|
||||
|
||||
if drift, reason := st.Drift(); drift {
|
||||
return doctor.CheckResult{Status: doctor.StatusFail, Message: reason}
|
||||
}
|
||||
|
||||
if st.Trusted() {
|
||||
if st.ExpiringSoon {
|
||||
return doctor.CheckResult{Status: doctor.StatusWarn, Message: "CA trusted but expiring within 30 days"}
|
||||
}
|
||||
return doctor.CheckResult{Status: doctor.StatusPass, Message: "MITM CA installed and trusted"}
|
||||
}
|
||||
|
||||
// On disk but not trusted in any store.
|
||||
if !userScopeSupported {
|
||||
// Linux: env-var injection (SSL_CERT_FILE) already covers Go; store trust optional.
|
||||
return doctor.CheckResult{Status: doctor.StatusWarn, Message: "CA on disk; not in OS store (Linux uses SSL_CERT_FILE; --system for store trust)"}
|
||||
}
|
||||
return doctor.CheckResult{Status: doctor.StatusFail, Message: "CA on disk but not trusted; run pmg setup cert install"}
|
||||
}
|
||||
|
||||
func printResults(results []doctor.CheckResult) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safedep/pmg/internal/doctor"
|
||||
"github.com/safedep/pmg/proxy/certmanager"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCACheckAbsentIsWarn(t *testing.T) {
|
||||
res := evaluateCACheck(t.TempDir(), false, false, true)
|
||||
assert.Equal(t, doctor.StatusWarn, res.Status)
|
||||
}
|
||||
|
||||
func TestCACheckTrustedIsPass(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ca, err := certmanager.GenerateCA(certmanager.PersistentCACertManagerConfig())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, certmanager.SaveCA(dir, ca))
|
||||
|
||||
res := evaluateCACheck(dir, true, false, true)
|
||||
assert.Equal(t, doctor.StatusPass, res.Status)
|
||||
}
|
||||
|
||||
func TestCACheckOnDiskNotTrustedMacWinIsFail(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ca, err := certmanager.GenerateCA(certmanager.PersistentCACertManagerConfig())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, certmanager.SaveCA(dir, ca))
|
||||
|
||||
res := evaluateCACheck(dir, false, false, true) // userScopeSupported=true (mac/win)
|
||||
assert.Equal(t, doctor.StatusFail, res.Status)
|
||||
}
|
||||
|
||||
func TestCACheckOnDiskNotTrustedLinuxIsWarn(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ca, err := certmanager.GenerateCA(certmanager.PersistentCACertManagerConfig())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, certmanager.SaveCA(dir, ca))
|
||||
|
||||
res := evaluateCACheck(dir, false, false, false) // userScopeSupported=false (linux)
|
||||
assert.Equal(t, doctor.StatusWarn, res.Status)
|
||||
}
|
||||
@@ -15,7 +15,9 @@ import (
|
||||
"github.com/safedep/pmg/internal/audit"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"github.com/safedep/pmg/internal/version"
|
||||
"github.com/safedep/pmg/proxy/certmanager"
|
||||
"github.com/safedep/pmg/sandbox/platform"
|
||||
"github.com/safedep/pmg/truststore"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -143,6 +145,26 @@ func executeSetupInfo() error {
|
||||
|
||||
ui.PrintInfoSection("Sandbox", sandboxEntries)
|
||||
|
||||
// Certificate Authority section
|
||||
caStatus, _ := certmanager.InspectCA(cfg.ConfigDir())
|
||||
caUser, caSystem, _ := truststore.Status(certmanager.CACommonName)
|
||||
caStatus.UserTrusted, caStatus.SystemTrusted = caUser, caSystem
|
||||
|
||||
caEntries := make(map[string]string)
|
||||
caEntries["Installed"] = strconv.FormatBool(caStatus.KeyPresent && caStatus.CertPresent)
|
||||
caScope := "none"
|
||||
if caStatus.SystemTrusted {
|
||||
caScope = "system"
|
||||
} else if caStatus.UserTrusted {
|
||||
caScope = "user"
|
||||
}
|
||||
caEntries["Trust Scope"] = caScope
|
||||
if caStatus.CertPresent {
|
||||
caEntries["Expires"] = caStatus.NotAfter.Format("2006-01-02")
|
||||
caEntries["Fingerprint"] = caStatus.Fingerprint
|
||||
}
|
||||
ui.PrintInfoSection("Certificate Authority", caEntries)
|
||||
|
||||
if cfg.Config.Cloud.Enabled {
|
||||
cloudEntries := make(map[string]string)
|
||||
cloudEntries["Enabled"] = "true"
|
||||
|
||||
@@ -28,6 +28,7 @@ func NewSetupCommand() *cobra.Command {
|
||||
setupCmd.AddCommand(NewRemoveCommand())
|
||||
setupCmd.AddCommand(NewInfoCommand())
|
||||
setupCmd.AddCommand(NewDoctorCommand())
|
||||
setupCmd.AddCommand(NewCertCommand())
|
||||
|
||||
return setupCmd
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user