#!/bin/bash # # FileWave Server (macOS / Linux): remove the compromised eMudhra emSign G3 root certificates from the # CA bundle used by the FileWave Python (Django, scheduler, model update, ...). # # Background: in October 2026 the private keys of eight eMudhra intermediate CAs were # compromised. FileWave Python verifies outbound HTTPS against the bundled certifi CA file, # which does not honour browser revocation lists, so the emSign G3 roots are removed from it, as upstream certifi # 2026.10.19 does. The other emSign roots (G1, C1, C3) are not affected and are kept. # The FileWave additions to that file (Apple Root CA for gdmf.apple.com) are kept. # # Usage: sudo ./fw_server_remove_emsign.sh [--dry-run] [--no-restart] [--file PATH] # --dry-run only report what would be removed # --no-restart do not restart the FileWave services (restart them later yourself: # sudo /usr/local/sbin/fwcontrol server restart) # --file PATH clean this CA file instead of the FileWave Python certifi bundle # # A backup of the original file is written next to it (cacert.pem.bak-emsign-). # The script is idempotent: running it again changes nothing. set -euo pipefail FWPYTHON=/usr/local/filewave/python/bin/python FWCONTROL=/usr/local/sbin/fwcontrol DRY_RUN=0 RESTART=1 FILES=() usage() { sed -n '2,20p' "$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 [ ${#FILES[@]} -eq 0 ]; then for f in /usr/local/filewave/python/lib/python3*/site-packages/certifi/cacert.pem; do [ -f "$f" ] && FILES+=("$f") done fi [ ${#FILES[@]} -gt 0 ] || { echo "ERROR: FileWave Python certifi bundle not found, is this a FileWave Server?" >&2; exit 1; } 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 clean_file "$f" "$(dirname "$f")" grep -q "Added by FileWave build step" "$f" \ || echo " WARNING: the FileWave Apple Root CA addition is not in this file, gdmf.apple.com calls may fail" done if [ "$CHANGED" -eq 1 ] && [ -x "$FWPYTHON" ]; then echo "Verifying the cleaned bundle loads in FileWave Python..." for f in "${FILES[@]}"; do "$FWPYTHON" -I -c 'import ssl, sys; c = ssl.create_default_context(cafile=sys.argv[1]); print(" OK,", len(c.get_ca_certs()), "CA certificates in", sys.argv[1])' "$f" done fi if [ -f /private/var/FileWave/trust_store/fw_cacert.pem ] || [ -f /var/FileWave/trust_store/fw_cacert.pem ]; then echo "NOTE: a FileWave Booster is installed on this host, also run fw_booster_remove_emsign.sh" fi if [ "$CHANGED" -eq 1 ]; then if [ "$RESTART" -eq 1 ]; then echo "Restarting the FileWave Server so every Python process reloads the bundle..." "$FWCONTROL" server restart else echo "Restart the FileWave Server to apply: sudo $FWCONTROL server restart" fi fi echo "Done."