#!/bin/bash # # FileWave Booster (macOS / Linux): remove the compromised eMudhra emSign G3 root certificates from the # booster trust store (fw_cacert.pem). # # Background: in October 2026 the private keys of eight eMudhra intermediate CAs were # compromised. The booster trusts every certificate in its trust_store folder, plus the OS # trust store, so the emSign G3 roots are removed from fw_cacert.pem, as upstream certifi 2026.10.19 # does, and the OS store is checked. The other emSign roots (G1, C1, C3) are not affected. # # Usage: sudo ./fw_booster_remove_emsign.sh [--dry-run] [--no-restart] [--file PATH] # --dry-run only report what would be removed # --no-restart do not restart the booster (restart it later yourself: # sudo /usr/local/sbin/fwcontrol booster restart) # --file PATH clean this CA file instead of the booster trust store file # # The backup of fw_cacert.pem is written OUTSIDE the trust_store folder, because the booster # loads every file in that folder. The script is idempotent: running it again changes nothing. set -euo pipefail FWCONTROL=/usr/local/sbin/fwcontrol DRY_RUN=0 RESTART=1 FILES=() usage() { sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; } while [ $# -gt 0 ]; do case "$1" in --dry-run) DRY_RUN=1 ;; --no-restart) RESTART=0 ;; --file) shift; FILES+=("${1:?--file needs a path}") ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage; exit 2 ;; esac shift done command -v openssl >/dev/null || { echo "ERROR: openssl not found in PATH" >&2; exit 1; } if [ "$(uname)" = "Darwin" ]; then TRUST_STORE_FILE=/private/var/FileWave/trust_store/fw_cacert.pem else TRUST_STORE_FILE=/var/FileWave/trust_store/fw_cacert.pem fi if [ ${#FILES[@]} -eq 0 ]; then [ -f "$TRUST_STORE_FILE" ] || { echo "ERROR: $TRUST_STORE_FILE not found, is this a FileWave Booster?" >&2; exit 1; } FILES+=("$TRUST_STORE_FILE") # The Linux booster also ships FileWave Python (supervisord); clean its bundle as well. for f in /usr/local/filewave/python/lib/python3*/site-packages/certifi/cacert.pem; do [ -f "$f" ] && FILES+=("$f") done fi REMOVED=0 KEPT=0 CHANGED=0 # The roots excluded by certifi 2026.10.19; the TLS one is not in certifi but may be in an OS store. AFFECTED_ROOTS='CN ?= ?emSign (ECC Root CA|Root TLS CA) - G3' # filter_bundle IN OUT: writes IN to OUT without the affected roots (and their comment headers) filter_bundle() { local in="$1" out="$2" chunk="" pem="" in_pem=0 line subject REMOVED=0 KEPT=0 : > "$out" while IFS= read -r line || [ -n "$line" ]; do chunk+="$line"$'\n' if [ "$line" = "-----BEGIN CERTIFICATE-----" ]; then in_pem=1 pem="$line"$'\n' elif [ "$in_pem" -eq 1 ] && [ "$line" = "-----END CERTIFICATE-----" ]; then in_pem=0 pem+="$line"$'\n' subject=$(printf '%s' "$pem" | openssl x509 -noout -subject 2>/dev/null || true) if printf '%s' "$subject" | grep -qE "$AFFECTED_ROOTS"; then echo " affected root: ${subject#subject=}" REMOVED=$((REMOVED + 1)) else printf '%s' "$chunk" >> "$out" KEPT=$((KEPT + 1)) fi chunk="" elif [ "$in_pem" -eq 1 ]; then pem+="$line"$'\n' fi done < "$in" printf '%s' "$chunk" >> "$out" } # clean_file FILE BACKUP_DIR clean_file() { local file="$1" backup_dir="$2" tmp backup echo "Checking $file" [ -f "$file" ] || { echo " ERROR: file not found" >&2; return 1; } if [ "$DRY_RUN" -eq 0 ] && { [ ! -w "$file" ] || [ ! -w "$backup_dir" ]; }; then echo " ERROR: no write access, run with sudo" >&2 return 1 fi tmp=$(mktemp "$backup_dir/.emsign.XXXXXX") cp -p "$file" "$tmp" filter_bundle "$file" "$tmp" if [ "$REMOVED" -eq 0 ]; then echo " no affected emSign root found, nothing to do" rm -f "$tmp" return 0 fi if [ "$KEPT" -eq 0 ]; then echo " ERROR: nothing would be left in the file, left untouched" >&2 rm -f "$tmp" return 1 fi if [ "$DRY_RUN" -eq 1 ]; then echo " dry run: would remove $REMOVED certificate(s) and keep $KEPT" rm -f "$tmp" return 0 fi backup="$backup_dir/$(basename "$file").bak-emsign-$(date +%Y%m%d%H%M%S)" cp -p "$file" "$backup" mv -f "$tmp" "$file" CHANGED=1 echo " removed $REMOVED certificate(s), kept $KEPT, backup: $backup" } for f in "${FILES[@]}"; do if [ "$(basename "$(dirname "$f")")" = "trust_store" ]; then clean_file "$f" "$(dirname "$(dirname "$f")")" else clean_file "$f" "$(dirname "$f")" fi done echo "Checking the OS trust store (the booster trusts it too)..." os_count=0 if [ "$(uname)" = "Darwin" ]; then os_count=$(security find-certificate -a -c "emSign" \ /System/Library/Keychains/SystemRootCertificates.keychain /Library/Keychains/System.keychain 2>/dev/null \ | grep '"labl"' | grep -cE 'emSign (ECC Root CA|Root TLS CA) - G3' || true) else for bundle in /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt; do if [ -f "$bundle" ]; then filter_bundle "$bundle" /dev/null os_count=$REMOVED break fi done fi if [ "$os_count" -gt 0 ]; then echo " WARNING: the OS trust store still holds $os_count affected emSign root(s), the booster keeps trusting them." echo " Install the latest OS updates (Linux: the ca-certificates package) once the vendor ships the removal." else echo " OK, no affected emSign root in the OS trust store" fi if [ "$CHANGED" -eq 1 ]; then if [ "$RESTART" -eq 1 ]; then echo "Restarting the FileWave Booster..." if command -v systemctl >/dev/null && systemctl cat fw-booster.service >/dev/null 2>&1; then systemctl restart fw-booster.service else "$FWCONTROL" booster restart fi else echo "Restart the booster to apply: sudo $FWCONTROL booster restart" fi fi echo "Done."