summaryrefslogtreecommitdiff
path: root/internal/cert/install.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/cert/install.go')
-rw-r--r--internal/cert/install.go149
1 files changed, 149 insertions, 0 deletions
diff --git a/internal/cert/install.go b/internal/cert/install.go
new file mode 100644
index 0000000..8ea25b9
--- /dev/null
+++ b/internal/cert/install.go
@@ -0,0 +1,149 @@
+package cert
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+// InstallCA attempts to install the Shiv CA into the user-level trust store.
+// Never requires sudo. Returns a human-readable status string, or an error
+// with manual instructions if automatic installation is not possible.
+func (ca *CA) InstallCA() (string, error) {
+ certPath, err := CertPEMPath()
+ if err != nil {
+ return "", err
+ }
+
+ switch runtime.GOOS {
+ case "linux":
+ return installLinux(certPath)
+ case "darwin":
+ return installDarwin(certPath)
+ case "windows":
+ return installWindows(certPath)
+ default:
+ return "", manualInstructions(certPath, fmt.Sprintf("unsupported OS: %s", runtime.GOOS))
+ }
+}
+
+func installLinux(certPath string) (string, error) {
+ if _, err := exec.LookPath("certutil"); err != nil {
+ return "", manualInstructions(certPath, "certutil not found (install libnss3-tools on Debian/Ubuntu or nss on Arch)")
+ }
+
+ var installed []string
+ var errs []string
+
+ // Chrome/Chromium user NSS database.
+ home, err := os.UserHomeDir()
+ if err == nil {
+ nssdb := filepath.Join(home, ".pki", "nssdb")
+ if err := os.MkdirAll(nssdb, 0700); err == nil {
+ cmd := exec.Command("certutil", "-A", "-n", "Shiv CA", "-t", "CT,,", "-i", certPath, "-d", "sql:"+nssdb)
+ if cmdOut, err := cmd.CombinedOutput(); err != nil {
+ errs = append(errs, fmt.Sprintf("Chrome NSS: %v\n%s", err, cmdOut))
+ } else {
+ installed = append(installed, "Chrome/Chromium")
+ }
+ }
+ }
+
+ // Firefox profiles.
+ profiles, _ := firefoxProfiles()
+ for _, profile := range profiles {
+ cmd := exec.Command("certutil", "-A", "-n", "Shiv CA", "-t", "CT,,", "-i", certPath, "-d", profile)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ errs = append(errs, fmt.Sprintf("Firefox profile %s: %v\n%s", profile, err, out))
+ } else {
+ installed = append(installed, "Firefox ("+filepath.Base(profile)+")")
+ }
+ }
+
+ if len(installed) == 0 {
+ msg := "certutil ran but no stores were updated"
+ if len(errs) > 0 {
+ msg = strings.Join(errs, "\n")
+ }
+ return "", manualInstructions(certPath, msg)
+ }
+
+ result := "CA certificate installed for: " + strings.Join(installed, ", ") + "."
+ if len(errs) > 0 {
+ result += "\nSome stores failed:\n" + strings.Join(errs, "\n")
+ }
+ return result, nil
+}
+
+func installDarwin(certPath string) (string, error) {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", manualInstructions(certPath, fmt.Sprintf("cannot find home directory: %v", err))
+ }
+
+ loginKeychain := filepath.Join(home, "Library", "Keychains", "login.keychain-db")
+ cmd := exec.Command("security", "add-trusted-cert",
+ "-r", "trustRoot",
+ "-k", loginKeychain,
+ certPath,
+ )
+ if out, err := cmd.CombinedOutput(); err != nil {
+ return "", manualInstructions(certPath, fmt.Sprintf("security add-trusted-cert failed: %v\n%s", err, out))
+ }
+ return "CA certificate installed to macOS login keychain.", nil
+}
+
+func installWindows(certPath string) (string, error) {
+ // certutil is built into Windows, no extra tools needed.
+ // -user flag installs to current user store, no admin required.
+ cmd := exec.Command("certutil", "-addstore", "-user", "-f", "ROOT", certPath)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ return "", manualInstructions(certPath, fmt.Sprintf("certutil failed: %v\n%s", err, out))
+ }
+ return "CA certificate installed to Windows user root store.", nil
+}
+
+func firefoxProfiles() ([]string, error) {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return nil, err
+ }
+
+ var profiles []string
+
+ profileDirs := []string{
+ filepath.Join(home, ".mozilla", "firefox"),
+ filepath.Join(home, ".librewolf"),
+ filepath.Join(home, "snap", "firefox", "common", ".mozilla", "firefox"),
+ filepath.Join(home, ".var", "app", "org.mozilla.firefox", ".mozilla", "firefox"),
+ filepath.Join(home, ".var", "app", "io.gitlab.librewolf-community", ".librewolf"),
+ }
+
+ for _, dir := range profileDirs {
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ continue
+ }
+ for _, dirEntry := range entries {
+ if dirEntry.IsDir() {
+ profiles = append(profiles, filepath.Join(dir, dirEntry.Name()))
+ }
+ }
+ }
+
+ return profiles, nil
+}
+
+func manualInstructions(certPath, reason string) error {
+ return errors.New(reason + "\n\n" +
+ "To install manually, import this file into your browser:\n" +
+ " " + certPath + "\n\n" +
+ "Browser instructions:\n" +
+ " Chrome/Chromium: Settings → Privacy → Security → Manage certificates → Authorities → Import\n" +
+ " Firefox: Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import\n" +
+ " NixOS: security.pki.certificateFiles = [ \"" + certPath + "\" ]")
+}