Files
pmg/ebpf-poc/ca.go
T
Sahilb315 9b29a95e03 feat(ebpf-poc): trust the CA machine wide instead of configuring npm
v1 owns one trust mechanism: the system trust store. Every user and every
program that reads it gets the proxy CA with no configuration. Programs
that carry their own certificate list are documented rather than managed,
because each keeps that list in a different place and format.

ca install now calls truststore.Install with system scope and verifies the
result. ca remove untrusts before deleting, since a CA left trusted after
its files are gone is worse than one never installed: nothing points at
it, but whoever holds the old key can still intercept every user.

Only the public certificate reaches the trust store. The private key stays
0600 under the proxy user, which is the entire security boundary here.

Deletes npm_ca.go and the npm half of ca.go: the per user cafile handling,
the previous value save and restore, the read back verification, and the
--npm-user and --npm-bin flags.

Verified on Ubuntu with no per tool configuration anywhere: curl without
--cacert, npm without cafile, and a malicious package still blocked with
403. Removal confirmed to leave zero matches in ca-certificates.crt.
2026-07-28 19:46:46 +05:30

497 lines
14 KiB
Go

package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/safedep/pmg/proxy/certmanager"
"github.com/safedep/pmg/truststore"
)
const defaultCAStateDir = "/var/lib/pmg-ebpf-poc"
// systemAccount is the identity the proxy runs as. The CA private key is owned
// by it and readable by nobody else.
type systemAccount struct {
Name string
UID uint32
GID uint32
HomeDir string
}
type caCommandOptions struct {
proxyUser string
proxyConfigDir string
bundlePath string
statePath string
proxyStatePath string
}
type caState struct {
ProxyUser string `json:"proxy_user"`
ProxyConfigDir string `json:"proxy_config_dir"`
BundlePath string `json:"bundle_path"`
CAFingerprint string `json:"ca_fingerprint"`
CACreated bool `json:"ca_created"`
ProxyStatePath string `json:"proxy_state_path"`
}
type caStatus struct {
CAValid bool
KeyModeValid bool
BundleValid bool
SystemTrust bool
Fingerprint string
}
func runCACommand(args []string, out io.Writer) error {
if len(args) == 0 || args[0] == "help" || args[0] == "--help" || args[0] == "-h" {
return printCAUsage(out)
}
if len(args) == 2 && (args[1] == "--help" || args[1] == "-h") {
return printCAUsage(out)
}
switch args[0] {
case "install":
opts, err := parseCAOptions("install", args[1:])
if err != nil {
return err
}
if err := requireRoot(); err != nil {
return err
}
state, err := installCA(opts)
if err != nil {
return err
}
_, err = fmt.Fprintf(out,
"Persistent CA: %s\nPrivate key owner: %s\nSystem trust store: installed\nPublic bundle: %s\nFingerprint: %s\n",
certmanager.CACertPath(state.ProxyConfigDir), state.ProxyUser,
state.BundlePath, state.CAFingerprint)
return err
case "status":
opts, err := parseCAOptions("status", args[1:])
if err != nil {
return err
}
if err := requireRoot(); err != nil {
return err
}
status, err := inspectCA(opts)
if err != nil {
return err
}
if err := printCAStatus(out, status); err != nil {
return err
}
if !status.CAValid || !status.KeyModeValid || !status.BundleValid || !status.SystemTrust {
return errors.New("CA setup is unhealthy")
}
return nil
case "remove":
opts, err := parseCAOptions("remove", args[1:])
if err != nil {
return err
}
if err := requireRoot(); err != nil {
return err
}
if err := removeCA(opts); err != nil {
return err
}
_, err = fmt.Fprintln(out, "CA removed from the system trust store and deleted")
return err
default:
return fmt.Errorf("unknown CA command %q, expected install, status, or remove", args[0])
}
}
func printCAUsage(out io.Writer) error {
_, err := fmt.Fprintln(out, `Usage:
pmgwatch ca install [--proxy-user pmg-proxy]
pmgwatch ca status
pmgwatch ca remove
install generates or reuses the proxy CA and trusts it machine wide.
status verifies the CA files, key ownership, public bundle, and system trust.
remove untrusts the CA and deletes the files this command created.
Tools that ignore the system trust store, such as Node from nodejs.org,
bun and uv, need their own configuration. Point them at the public bundle
this command writes. See SETUP.md.`)
return err
}
func parseCAOptions(name string, args []string) (caCommandOptions, error) {
var opts caCommandOptions
flags := flag.NewFlagSet("ca "+name, flag.ContinueOnError)
flags.SetOutput(io.Discard)
flags.StringVar(&opts.proxyUser, "proxy-user", "pmg-proxy", "user running the PMG proxy")
flags.StringVar(&opts.proxyConfigDir, "proxy-config-dir", "", "PMG proxy config directory")
flags.StringVar(&opts.bundlePath, "bundle", filepath.Join(defaultCAStateDir, "pmg-ca-bundle.pem"),
"public CA bundle for tools that do not read the system trust store")
flags.StringVar(&opts.statePath, "state", filepath.Join(defaultCAStateDir, "ca-state.json"), "CA setup state")
flags.StringVar(&opts.proxyStatePath, "proxy-state", "", "PMG proxy state file used to guard changes")
if err := flags.Parse(args); err != nil {
return caCommandOptions{}, err
}
if flags.NArg() != 0 {
return caCommandOptions{}, fmt.Errorf("unexpected arguments: %s", strings.Join(flags.Args(), " "))
}
return opts, nil
}
func installCA(opts caCommandOptions) (*caState, error) {
proxyAccount, err := lookupSystemAccount(opts.proxyUser)
if err != nil {
return nil, fmt.Errorf("resolve proxy user: %w", err)
}
resolveCAPaths(&opts, proxyAccount)
if err := ensureProxyStopped(opts.proxyStatePath); err != nil {
return nil, err
}
state, err := loadCAState(opts.statePath)
switch {
case err == nil:
if err := state.matches(opts); err != nil {
return nil, err
}
case errors.Is(err, os.ErrNotExist):
state = &caState{
ProxyUser: opts.proxyUser,
ProxyConfigDir: opts.proxyConfigDir,
BundlePath: opts.bundlePath,
ProxyStatePath: opts.proxyStatePath,
}
default:
return nil, err
}
ca, created, err := ensurePersistentCA(opts.proxyConfigDir, proxyAccount)
if err != nil {
return nil, err
}
state.CACreated = state.CACreated || created
if err := writeClientBundle(opts.bundlePath, ca.Certificate); err != nil {
return nil, err
}
// Only the public certificate reaches the trust store. The private key
// never leaves the proxy's config directory.
if err := truststore.Install(ca.Certificate, truststore.ScopeSystem); err != nil {
return nil, fmt.Errorf("install CA into the system trust store: %w", err)
}
if _, trusted, err := truststore.Status(certmanager.CACommonName); err != nil {
return nil, fmt.Errorf("verify system trust: %w", err)
} else if !trusted {
return nil, errors.New("CA is not trusted after install")
}
state.CAFingerprint = certificateFingerprint(ca)
if err := saveCAState(opts.statePath, state); err != nil {
return nil, err
}
return state, nil
}
func inspectCA(opts caCommandOptions) (caStatus, error) {
state, err := loadCAState(opts.statePath)
if err != nil {
return caStatus{}, err
}
opts = state.options(opts.statePath)
proxyAccount, err := lookupSystemAccount(opts.proxyUser)
if err != nil {
return caStatus{}, err
}
status := caStatus{}
ca, err := certmanager.LoadCA(opts.proxyConfigDir)
if err == nil && !ca.IsExpired(0) {
status.Fingerprint = certificateFingerprint(ca)
status.CAValid = status.Fingerprint == state.CAFingerprint
}
keyInfo, keyErr := os.Stat(certmanager.CAKeyPath(opts.proxyConfigDir))
if keyErr == nil {
keyStat, ok := keyInfo.Sys().(*syscall.Stat_t)
status.KeyModeValid = ok && keyInfo.Mode().Perm() == 0o600 &&
keyStat.Uid == proxyAccount.UID && keyStat.Gid == proxyAccount.GID
}
if status.CAValid {
bundle, bundleErr := os.ReadFile(opts.bundlePath)
status.BundleValid = bundleErr == nil && bytes.Contains(bundle, ca.Certificate)
}
_, trusted, err := truststore.Status(certmanager.CACommonName)
if err != nil {
return caStatus{}, fmt.Errorf("read system trust store: %w", err)
}
status.SystemTrust = trusted
return status, nil
}
func removeCA(opts caCommandOptions) error {
state, err := loadCAState(opts.statePath)
if err != nil {
return err
}
opts = state.options(opts.statePath)
if err := ensureProxyStopped(opts.proxyStatePath); err != nil {
return err
}
// Untrust first. A CA left in the machine trust store after its files are
// gone is worse than one that was never installed: nothing points at it,
// but anyone holding the old key can still intercept every user.
if err := truststore.Uninstall(certmanager.CACommonName, truststore.ScopeSystem); err != nil {
return fmt.Errorf("remove CA from the system trust store: %w", err)
}
paths := []string{opts.bundlePath}
if state.CACreated {
paths = append(paths,
certmanager.CACertPath(opts.proxyConfigDir),
certmanager.CAKeyPath(opts.proxyConfigDir),
)
}
paths = append(paths, opts.statePath)
for _, path := range paths {
if err := removeFile(path); err != nil {
return err
}
}
return nil
}
func ensurePersistentCA(dir string, account systemAccount) (*certmanager.Certificate, bool, error) {
created := false
ca, err := certmanager.LoadCA(dir)
if err != nil {
status, inspectErr := certmanager.InspectCA(dir)
if inspectErr != nil {
return nil, false, inspectErr
}
if status.CertPresent || status.KeyPresent {
return nil, false, fmt.Errorf("persisted CA is incomplete or unreadable: %w", err)
}
ca, err = certmanager.GenerateCA(certmanager.PersistentCACertManagerConfig())
if err != nil {
return nil, false, fmt.Errorf("generate CA: %w", err)
}
if err := certmanager.SaveCA(dir, ca); err != nil {
return nil, false, fmt.Errorf("save CA: %w", err)
}
created = true
}
if ca.IsExpired(0) {
return nil, false, errors.New("persisted CA is expired")
}
// The private key is the whole security boundary: whoever can read it can
// mint a certificate for any domain that the machine now trusts.
for _, item := range []struct {
path string
mode os.FileMode
}{
{dir, 0o700},
{certmanager.CACertPath(dir), 0o644},
{certmanager.CAKeyPath(dir), 0o600},
} {
if err := os.Chmod(item.path, item.mode); err != nil {
return nil, false, fmt.Errorf("chmod %s: %w", item.path, err)
}
if err := os.Chown(item.path, int(account.UID), int(account.GID)); err != nil {
return nil, false, fmt.Errorf("chown %s: %w", item.path, err)
}
}
return ca, created, nil
}
// writeClientBundle writes the CA merged with the system roots. Settings such
// as npm's cafile replace the trust set rather than adding to it, so a client
// pointed at the bare CA would reject every other site.
func writeClientBundle(path string, caPEM []byte) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create bundle directory: %w", err)
}
if err := os.WriteFile(path, certmanager.MergeWithSystemCA(caPEM), 0o644); err != nil {
return fmt.Errorf("write CA bundle: %w", err)
}
if err := os.Chmod(path, 0o644); err != nil {
return fmt.Errorf("chmod CA bundle: %w", err)
}
return nil
}
func saveCAState(path string, state *caState) error {
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return fmt.Errorf("marshal CA state: %w", err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return fmt.Errorf("create CA state directory: %w", err)
}
if err := os.WriteFile(path, data, 0o600); err != nil {
return fmt.Errorf("write CA state: %w", err)
}
if err := os.Chmod(path, 0o600); err != nil {
return fmt.Errorf("chmod CA state: %w", err)
}
return nil
}
func loadCAState(path string) (*caState, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read CA state %q: %w", path, err)
}
var state caState
if err := json.Unmarshal(data, &state); err != nil {
return nil, fmt.Errorf("parse CA state %q: %w", path, err)
}
return &state, nil
}
func (s *caState) matches(opts caCommandOptions) error {
if s.ProxyUser != opts.proxyUser || s.ProxyConfigDir != opts.proxyConfigDir ||
s.BundlePath != opts.bundlePath {
return errors.New("existing CA state was created with different options; remove it before changing setup")
}
return nil
}
func (s *caState) options(statePath string) caCommandOptions {
return caCommandOptions{
proxyUser: s.ProxyUser,
proxyConfigDir: s.ProxyConfigDir,
bundlePath: s.BundlePath,
statePath: statePath,
proxyStatePath: s.ProxyStatePath,
}
}
func resolveCAPaths(opts *caCommandOptions, proxy systemAccount) {
if opts.proxyConfigDir == "" {
opts.proxyConfigDir = filepath.Join(proxy.HomeDir, ".config", "safedep", "pmg")
}
if opts.proxyStatePath == "" {
opts.proxyStatePath = filepath.Join(proxy.HomeDir, "state", "proxy.json")
}
}
// ensureProxyStopped guards changes to the CA. The proxy reads the certificate
// once at startup, so a running daemon would keep serving the old one.
func ensureProxyStopped(path string) error {
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return fmt.Errorf("read proxy state: %w", err)
}
var state struct {
PID int `json:"pid"`
}
if err := json.Unmarshal(data, &state); err != nil {
return fmt.Errorf("parse proxy state: %w", err)
}
if state.PID <= 0 {
return nil
}
process, err := os.FindProcess(state.PID)
if err == nil && process.Signal(syscall.Signal(0)) == nil {
return fmt.Errorf("proxy pid %d is still running; stop it before changing the CA setup", state.PID)
}
return nil
}
func printCAStatus(out io.Writer, status caStatus) error {
rows := []struct {
name string
ok bool
}{
{"Persistent CA", status.CAValid},
{"Private key access", status.KeyModeValid},
{"Public bundle", status.BundleValid},
{"System trust store", status.SystemTrust},
}
for _, row := range rows {
result := "FAIL"
if row.ok {
result = "PASS"
}
if _, err := fmt.Fprintf(out, "%-20s %s\n", row.name, result); err != nil {
return err
}
}
if status.Fingerprint != "" {
if _, err := fmt.Fprintf(out, "%-20s %s\n", "Fingerprint", status.Fingerprint); err != nil {
return err
}
}
return nil
}
func certificateFingerprint(ca *certmanager.Certificate) string {
sum := sha256.Sum256(ca.X509Cert.Raw)
return hex.EncodeToString(sum[:])
}
func lookupSystemAccount(name string) (systemAccount, error) {
account, err := user.Lookup(name)
if err != nil {
return systemAccount{}, err
}
uid, err := strconv.ParseUint(account.Uid, 10, 32)
if err != nil {
return systemAccount{}, fmt.Errorf("parse uid %q: %w", account.Uid, err)
}
gid, err := strconv.ParseUint(account.Gid, 10, 32)
if err != nil {
return systemAccount{}, fmt.Errorf("parse gid %q: %w", account.Gid, err)
}
return systemAccount{
Name: account.Username,
UID: uint32(uid),
GID: uint32(gid),
HomeDir: account.HomeDir,
}, nil
}
func requireRoot() error {
if os.Geteuid() != 0 {
return errors.New("run this command as root")
}
return nil
}
func removeFile(path string) error {
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove %s: %w", path, err)
}
return nil
}