Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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
125 changes: 112 additions & 13 deletions cac_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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


Expand Down Expand Up @@ -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
Loading