diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..97736d9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,186 @@ +# CLAUDE.md + +Guide for AI assistants working on the **linux_cac** repository. + +## Project Overview + +**linux_cac** is a Bash script that automates Common Access Card (CAC) configuration on Debian-based Linux distributions. It installs middleware, downloads DoD certificates, imports them into browser certificate databases, and registers the PKCS#11 module — enabling smart card authentication in Firefox and Chrome. + +The project uses OpenSC (migrated from Cackey) for PKCS#11 support. + +## Repository Structure + +``` +linux_cac/ +├── cac_setup.sh # Main script (single entry point, ~475 lines) +├── .github/workflows/CI.yml # GitHub Actions: ShellCheck static analysis +├── README.md # User-facing documentation +├── LICENSE # MIT License (2022-2025, Jeremy Jackson) +└── CLAUDE.md # This file +``` + +This is a single-script project. All logic lives in `cac_setup.sh`. + +## Language and Tooling + +- **Language:** Bash (requires `bash` 4.0+ for `mapfile`) +- **Linter:** [ShellCheck](https://www.shellcheck.net/) with `-x` flag (follow source directives) +- **CI:** GitHub Actions runs ShellCheck on all branches and PRs +- **No build step** — the script is distributed and executed directly + +## Running CI Locally + +```bash +shellcheck -x cac_setup.sh +``` + +This is the only automated quality check. All ShellCheck warnings must be resolved or explicitly suppressed with `# shellcheck disable=SCXXXX` comments. + +## Script Architecture + +`cac_setup.sh` follows a top-level `main()` function pattern — `main` is defined first and called at the bottom of the file (line 475). + +### Execution Flow + +1. `root_check()` — Verify root/sudo privileges +2. `browser_check()` — Detect Firefox (snap vs apt) and Chrome +3. If snap Firefox: prompt user to replace with apt version via `reconfigure_firefox()` +4. Locate `cert9.db` databases in user's home directory +5. Install middleware packages via `apt` +6. Download DoD certificates from `militarycac.com` +7. Import certificates into each browser's database via `certutil` +8. Register CAC module with `pkcs11-register` +9. Enable `pcscd.socket` service +10. Clean up temporary artifacts + +### Key Functions + +| Function | Purpose | +|---|---| +| `main()` | Orchestrates the full setup flow | +| `root_check()` | Ensures script runs as root (exit 86 if not) | +| `browser_check()` | Detects browsers and sets control flags | +| `check_for_firefox()` | Finds Firefox, determines snap vs apt | +| `check_for_chrome()` | Finds Google Chrome | +| `reconfigure_firefox()` | Replaces snap Firefox with apt version | +| `backup_ff_profile()` | Backs up Firefox profile before snap removal | +| `migrate_ff_profile()` | Restores profile after reinstall | +| `run_firefox()` / `run_chrome()` | Headless launch to initialize profile dirs | +| `import_certs()` | Imports .cer files into a cert9.db database | +| `check_for_ff_pin()` | Detects GNOME favorites bar pin | +| `repin_firefox()` | Re-pins Firefox after reinstall | +| `revert_firefox()` | Rolls back to snap Firefox on failure | +| `print_err()` / `print_info()` | Colored output helpers (red/yellow) | + +### Exit Codes + +| Code | Constant | Meaning | +|---|---|---| +| 0 | `EXIT_SUCCESS` | Successful completion | +| 86 | `E_NOTROOT` | Script not run as root | +| 87 | `E_BROWSER` | No compatible browser found | +| 88 | `E_DATABASE` | No cert9.db database located | + +### Key Variables + +- `ORIG_HOME` — The invoking user's home directory (resolved from `$SUDO_USER`) +- `DWNLD_DIR` — Temp directory for artifacts (`/tmp`) +- `DB_FILENAME` — Certificate database name (`cert9.db`) +- `CERT_URL` — DoD certificate bundle URL (HTTPS) +- `snap_ff` / `ff_exists` / `chrome_exists` — Boolean flags controlling flow + +## Code Conventions + +### Style + +- **Shebang:** `#!/usr/bin/env bash` +- **Function definitions:** Use `function_name ()` with opening brace on next line +- **Constants/globals:** `SCREAMING_SNAKE_CASE` (e.g., `EXIT_SUCCESS`, `DWNLD_DIR`) +- **Local/flag variables:** `lowercase_snake_case` (e.g., `snap_ff`, `chrome_exists`) +- **Booleans:** String comparison (`"$var" == true/false`) +- **Function closing:** Comment `} # function_name` after every closing brace +- **Quoting:** All variable expansions are quoted (`"$var"`) +- **Conditional style:** `if [ ... ]; then` or `if [ ... ]\nthen` (both used; multi-line `if/then` preferred) + +### ShellCheck Compliance + +The codebase must pass `shellcheck -x`. When a warning must be suppressed, use an inline directive with the specific code: + +```bash +# shellcheck disable=SC2016 +``` + +Only suppress warnings that are intentional (e.g., SC2016 for literal `$` in single-quoted strings meant for deferred evaluation). + +### Interactive Prompts + +The script prompts users interactively (y/n) for: +- Replacing snap Firefox with apt version +- Migrating Firefox profile data + +Prompts use a `while` loop validating input is exactly `"y"` or `"n"`. + +## System Dependencies + +Packages installed by the script: +``` +libpcsclite1 pcscd libccid libpcsc-perl pcsc-tools libnss3-tools unzip wget opensc +``` + +## Supported Configurations + +| Distribution | Versions | Browsers | +|---|---|---| +| Debian | 12.5 | Firefox ESR, Chrome, Edge | +| Mint | 21.2 | Firefox, Chrome | +| Parrot OS | 6.0.0-2 | Firefox, Brave | +| PopOS! | 20.04 LTS, 22.04 LTS | Firefox, Chrome | +| Ubuntu | 20.04 LTS, 22.04 LTS | Firefox, Chrome | + +## Git Workflow + +- **Default branch:** `main` +- **Branching:** Feature branches from `main` (e.g., `add-license`, `https-cert-url`) +- **Merges:** Pull requests into `main` +- **CI:** ShellCheck runs on all pushes and PRs + +## Contributing Guidelines + +Per README.md: +1. Fork the repository +2. Create a feature branch +3. Make focused, atomic commits +4. Ensure `shellcheck -x cac_setup.sh` passes +5. Open a PR against `main` +6. For larger changes, open an issue first to discuss + +## Common Tasks + +### Adding a new supported distribution +Update the table in `README.md` under "Supported Configurations" after testing. + +### Adding a new browser +1. Add a detection function (like `check_for_firefox` / `check_for_chrome`) +2. Add a headless runner to initialize the profile directory +3. Update `browser_check()` to call the new detection function +4. Ensure `cert9.db` path discovery in `main()` captures the new browser's database +5. Update README.md supported configurations + +### Modifying certificate import logic +The `import_certs()` function handles all cert importing. It takes a `cert9.db` path and uses `certutil` to import all `.cer` files with trust flags `TC`. + +### Suppressing a ShellCheck warning +Add a comment directly above the flagged line: +```bash +# shellcheck disable=SC1234 +problematic_line_here +``` +Document why the suppression is necessary. + +## Important Notes + +- The script must be run as root (`sudo`) because it installs system packages and modifies system services +- Snap Firefox is incompatible with the certificate import method — the script offers to replace it with the apt version from Mozilla's PPA +- The script downloads certificates from an external URL (`militarycac.com`) — changes to that upstream source may break functionality +- `pkcs11-register` behavior can be unreliable in scripted contexts (documented known issue) +- All temporary artifacts are stored in `/tmp` and cleaned up on exit diff --git a/cac_setup.sh b/cac_setup.sh index 38eec6e..1986a0b 100755 --- a/cac_setup.sh +++ b/cac_setup.sh @@ -9,12 +9,14 @@ main () E_NOTROOT=86 # Non-root exit error E_BROWSER=87 # Compatible browser not found E_DATABASE=88 # No database located + E_DISTRO=89 # Unsupported Linux distribution DWNLD_DIR="/tmp" # Location to place artifacts FF_PROFILE_NAME="old_ff_profile" # Location to save old Firefox profile chrome_exists=false # Google Chrome is installed ff_exists=false # Firefox is installed snap_ff=false # Flag to prompt for how to handle snap Firefox + OS_FAMILY="" # Detected OS family (debian/fedora/arch) ORIG_HOME="$(getent passwd "$SUDO_USER" | cut -d: -f6)" CERT_EXTENSION="cer" @@ -24,9 +26,18 @@ main () BUNDLE_FILENAME="AllCerts.zip" CERT_URL="https://militarycac.com/maccerts/$BUNDLE_FILENAME" + detect_os root_check browser_check - mapfile -t databases < <(find "$ORIG_HOME" -name "$DB_FILENAME" 2>/dev/null | grep "firefox\|pki" | grep -v "Trash\|snap") + + # Exclude snap paths only on Debian-family systems + if [ "$OS_FAMILY" == "debian" ] + then + mapfile -t databases < <(find "$ORIG_HOME" -name "$DB_FILENAME" 2>/dev/null | grep "firefox\|pki" | grep -v "Trash\|snap") + else + mapfile -t databases < <(find "$ORIG_HOME" -name "$DB_FILENAME" 2>/dev/null | grep "firefox\|pki" | grep -v "Trash") + fi + # Check if databases were found properly if [ "${#databases[@]}" -eq 0 ] then @@ -53,8 +64,7 @@ main () # Install middleware and necessary utilities print_info "Installing middleware and essential utilities..." - apt update - DEBIAN_FRONTEND=noninteractive apt install -y libpcsclite1 pcscd libccid libpcsc-perl pcsc-tools libnss3-tools unzip wget opensc + install_packages print_info "Done" # Pull all necessary files @@ -78,9 +88,7 @@ main () fi done - print_info "Registering CAC module with PKSC11..." - pkcs11-register - print_info "Done" + register_pkcs11 # NOTE: Keeping this temporarily to test `pkcs11-register`. # if ! grep -Pzo 'library=/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so\nname=CAC Module\n' "$db_root/$PKCS_FILENAME" >/dev/null @@ -90,6 +98,10 @@ main () print_info "Enabling pcscd service to start on boot..." systemctl enable pcscd.socket + if [ "$OS_FAMILY" != "debian" ] + then + systemctl start pcscd.socket + fi print_info "Done" # Remove artifacts @@ -343,18 +355,22 @@ migrate_ff_profile () # Attempt to find an installed version of Firefox on the user's system -# Determines whether the version is installed via snap or apt +# Determines whether the version is installed via snap or apt (Debian family only) check_for_firefox () { - if command -v firefox >/dev/null + local ff_path + ff_path="$(command -v firefox)" + if [ -n "$ff_path" ] + then + ff_exists=true + print_info "Found Firefox." + if [ "$OS_FAMILY" == "debian" ] then - ff_exists=true - print_info "Found Firefox." - if command -v firefox | grep snap >/dev/null + if echo "$ff_path" | grep snap >/dev/null then snap_ff=true print_err "This version of Firefox was installed as a snap package" - elif command -v firefox | xargs grep -Fq "exec /snap/bin/firefox" + elif grep -Fq "exec /snap/bin/firefox" "$ff_path" then snap_ff=true print_err "This version of Firefox was installed as a snap package with a launch script" @@ -365,8 +381,14 @@ check_for_firefox () print_info "Done." fi else - print_info "Firefox not found." + # Run Firefox to ensure .mozilla directory has been created + print_info "Running Firefox to generate profile directory..." + run_firefox + print_info "Done." fi + else + print_info "Firefox not found." + fi } # check_for_firefox @@ -472,4 +494,81 @@ repin_firefox () } # repin_firefox +# Detect the Linux distribution family and set OS_FAMILY accordingly +detect_os () +{ + if [ ! -f /etc/os-release ] + then + print_err "Cannot detect Linux distribution. /etc/os-release not found." + exit "$E_DISTRO" + fi + + # shellcheck source=/dev/null + . /etc/os-release + + local distro_id="${ID:-}" + local distro_like="${ID_LIKE:-}" + local distro_info="$distro_id $distro_like" + + if echo "$distro_info" | grep -qi "debian\|ubuntu" + then + OS_FAMILY="debian" + elif echo "$distro_info" | grep -qi "fedora\|rhel\|centos" + then + OS_FAMILY="fedora" + elif echo "$distro_info" | grep -qi "arch" + then + OS_FAMILY="arch" + else + print_err "Unsupported Linux distribution: ${PRETTY_NAME:-$distro_id}" + print_info "Supported distributions: Debian/Ubuntu, Fedora, Arch Linux" + exit "$E_DISTRO" + fi + + print_info "Detected OS: ${PRETTY_NAME:-$distro_id} (family: $OS_FAMILY)" +} # detect_os + + +# Install middleware packages using the appropriate package manager +install_packages () +{ + case "$OS_FAMILY" in + debian) + apt update + DEBIAN_FRONTEND=noninteractive apt install -y libpcsclite1 pcscd libccid libpcsc-perl pcsc-tools libnss3-tools unzip wget opensc + ;; + fedora) + dnf install -y pcsc-lite pcsc-lite-ccid opensc nss-tools unzip wget pcsc-tools + ;; + arch) + pacman -Sy --noconfirm pcsclite ccid opensc nss unzip wget pcsc-tools + ;; + esac +} # install_packages + + +# Register the CAC PKCS11 module using the appropriate method for the OS family +register_pkcs11 () +{ + case "$OS_FAMILY" in + debian) + print_info "Registering CAC module with PKCS11..." + pkcs11-register + print_info "Done" + ;; + fedora|arch) + print_info "Verifying PKCS11 module registration..." + # OpenSC module is automatically registered via p11-kit on Fedora and Arch + if p11-kit list-modules | grep -q opensc + then + print_info "OpenSC PKCS11 module is properly registered" + else + print_err "OpenSC PKCS11 module not found. You may need to reinstall opensc package." + fi + print_info "Done" + ;; + esac +} # register_pkcs11 + + main diff --git a/cac_setup_fedora.sh b/cac_setup_fedora.sh new file mode 100755 index 0000000..4bcd057 --- /dev/null +++ b/cac_setup_fedora.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash + +# cac_setup_fedora.sh +# Description: Setup a Fedora Linux environment for Common Access Card use. + +main () +{ + EXIT_SUCCESS=0 # Success exit code + E_NOTROOT=86 # Non-root exit error + E_BROWSER=87 # Compatible browser not found + E_DATABASE=88 # No database located + DWNLD_DIR="/tmp" # Location to place artifacts + + chrome_exists=false # Google Chrome is installed + ff_exists=false # Firefox is installed + + ORIG_HOME="$(getent passwd "$SUDO_USER" | cut -d: -f6)" + CERT_EXTENSION="cer" + DB_FILENAME="cert9.db" + CERT_FILENAME="AllCerts" + BUNDLE_FILENAME="AllCerts.zip" + CERT_URL="https://militarycac.com/maccerts/$BUNDLE_FILENAME" + + root_check + browser_check + mapfile -t databases < <(find "$ORIG_HOME" -name "$DB_FILENAME" 2>/dev/null | grep "firefox\|pki" | grep -v "Trash") + + # Check if databases were found properly + if [ "${#databases[@]}" -eq 0 ] + then + print_err "No valid databases located. Try running, then closing Firefox/Chrome, then start this script again." + echo -e "\tExiting..." + exit "$E_DATABASE" + fi + + # Install middleware and necessary utilities + print_info "Installing middleware and essential utilities..." + dnf install -y pcsc-lite pcsc-lite-ccid opensc nss-tools unzip wget pcsc-tools + print_info "Done" + + # Pull all necessary files + print_info "Downloading DoD certificates..." + wget -qP "$DWNLD_DIR" "$CERT_URL" + print_info "Done." + + # Unzip cert bundle + if [ -e "$DWNLD_DIR/$BUNDLE_FILENAME" ] + then + mkdir -p "$DWNLD_DIR/$CERT_FILENAME" + unzip -q "$DWNLD_DIR/$BUNDLE_FILENAME" -d "$DWNLD_DIR/$CERT_FILENAME" + fi + + # Import certificates into cert9.db databases for browsers + for db in "${databases[@]}" + do + if [ -n "$db" ] + then + import_certs "$db" + fi + done + + print_info "Verifying PKCS11 module registration..." + # On Fedora, OpenSC module is automatically registered via p11-kit + if p11-kit list-modules | grep -q opensc; then + print_info "OpenSC PKCS11 module is properly registered" + else + print_err "OpenSC PKCS11 module not found. You may need to reinstall opensc package." + fi + print_info "Done" + + print_info "Enabling pcscd service to start on boot..." + systemctl enable pcscd.socket + systemctl start pcscd.socket + print_info "Done" + + # Remove artifacts + print_info "Removing artifacts..." + rm -rf "${DWNLD_DIR:?}"/{,"$BUNDLE_FILENAME","$CERT_FILENAME"} 2>/dev/null + if [ "$?" -ne "$EXIT_SUCCESS" ] + then + print_err "Failed to remove artifacts. Artifacts were stored in ${DWNLD_DIR}." + else + print_info "Done. A reboot may be required." + fi + + exit "$EXIT_SUCCESS" +} # main + + +# Prints message with red [ERROR] tag before the message +print_err () +{ + ERR_COLOR='\033[0;31m' # Red for error messages + NO_COLOR='\033[0m' # Revert terminal back to no color + echo -e "${ERR_COLOR}[ERROR]${NO_COLOR} $1" +} # print_err + + +# Prints message with yellow [INFO] tag before the message +print_info () +{ + INFO_COLOR='\033[0;33m' # Yellow for notes + NO_COLOR='\033[0m' # Revert terminal back to no color + echo -e "${INFO_COLOR}[INFO]${NO_COLOR} $1" +} # print_info + + +# Check to ensure the script is executed as root +root_check () +{ + # Only users with $UID 0 have root privileges + local ROOT_UID=0 + + # Ensure the script is ran as root + if [ "${EUID:-$(id -u)}" -ne "$ROOT_UID" ] + then + print_err "Please run this script as root." + exit "$E_NOTROOT" + fi +} # root_check + + +# Run Firefox to ensure the profile directory has been created +run_firefox () +{ + print_info "Starting Firefox silently to complete post-install actions..." + sudo -H -u "$SUDO_USER" firefox --headless --first-startup >/dev/null 2>&1 & + sleep 3 + pkill -9 firefox + sleep 1 +} # run_firefox + + +# Run Chrome to ensure .pki directory has been created +run_chrome () +{ + print_info "Running Chrome to ensure it has completed post-install actions..." + sudo -H -u "$SUDO_USER" google-chrome --headless --disable-gpu >/dev/null 2>&1 & + sleep 3 + pkill -9 google-chrome + sleep 1 + print_info "Done." +} # run_chrome + + +# Discovery of browsers installed on the user's system +browser_check () +{ + print_info "Checking for Firefox and Chrome..." + check_for_firefox + check_for_chrome + + # Browser check results + if [ "$ff_exists" == false ] && [ "$chrome_exists" == false ] + then + print_err "No version of Mozilla Firefox OR Google Chrome has been detected." + print_info "Please install either or both to proceed." + exit "$E_BROWSER" + fi +} # browser_check + + +# Attempt to find an installed version of Firefox on the user's system +check_for_firefox () +{ + if command -v firefox >/dev/null + then + ff_exists=true + print_info "Found Firefox." + # Run Firefox to ensure .mozilla directory has been created + print_info "Running Firefox to generate profile directory..." + run_firefox + print_info "Done." + else + print_info "Firefox not found." + fi +} # check_for_firefox + + +# Attempt to find a version of Google Chrome installed on the user's system +check_for_chrome () +{ + # Check to see if Chrome exists + if command -v google-chrome >/dev/null + then + chrome_exists=true + print_info "Found Google Chrome." + # Run Chrome to ensure .pki directory has been created + run_chrome + else + print_info "Chrome not found." + fi +} # check_for_chrome + + +# Integrate all certificates into the databases for existing browsers +import_certs () +{ + db=$1 + db_root="$(dirname "$db")" + if [ -n "$db_root" ] + then + case "$db_root" in + *"pki"*) + print_info "Importing certificates for Chrome..." + echo + ;; + *"firefox"*) + print_info "Importing certificates for Firefox..." + echo + ;; + esac + + print_info "Loading certificates into $db_root " + echo + + for cert in "$DWNLD_DIR/$CERT_FILENAME/"*."$CERT_EXTENSION" + do + echo "Importing $cert" + certutil -d sql:"$db_root" -A -t TC -n "$cert" -i "$cert" + done + fi + + print_info "Done." + echo +} # import_certs + + +main