#!/bin/bash #set -x #################################################################################################### # # # SmartCardMapping # # Purpose:Maps the PIV UPN from a smartcard to the AltSecurityIdentities attribute for local accounts # # https://github.com/cocopuff2u # # Note: Tested only with government CAC/smartcards/yubikeys, uses swiftdialog or OSAscript for GUI # #################################################################################################### # # History # # 1.0 03/10/25 - Original # # 1.1 03/10/25 - /etc/SmartcardLogin.plist may exist already, having the script recreate it on # every run to resolve issues with the smartcard not being recognized. # # 1.2 05/27/25 - Added filevault option for M1 Arch, logging, and osascript dialog support # #. 1.3 06/13/25 - Improved OSAScript prompt handling, refined swiftDialog checks, added missing user # prompts,and updated default variables for enhanced usability. # # 1.4 6/17/25 - Added the EXEMPT_GROUP value to account for exempt user workflow. # # 1.5 6/24/25 - Added all DoD Trusted Authorities to /etc/SmartcardLogin.plist, this will allow # admins to use the checkCertificateTrust function to verify the smartcard with DoD # # 1.6 10/31/25 - Fixed the script to function from another admin user and not just the logged in user # #################################################################################################### #################################################################################################### # USER CONFIGURATION SECTION #################################################################################################### # Set to "true" to uninstall (Default is false) UNINSTALL=false # Set to "true" to enable logging to /var/log, "false" to disable (Default is true) ENABLE_LOGGING=true # Set to "false" to use SwiftDialog for dialogs instead of OSAScript (Default is true) # Its recommended to use swiftDialog for better user experience USE_OSASCRIPT=true # Set to "true" to include DoD Trusted Authorities in SmartcardLogin.plist (Default is true) # This will grab the latest DoD Trusted Authorities and add them to the SmartcardLogin.plist # The DoD Ceritificates should be deployed to the system via a profile or manually and ensured they # are trusted by the system for this to function properly. INCLUDE_TRUSTED_AUTHORITIES=true #################################################################################################### Script_Name="SmartcardMapping" LOG_FILE="/var/log/smartcard_mapping.log" Script_Version="1.2" # Logging function log_message() { local timestamp=$(date '+%Y-%m-%d %I:%M %p') local message="[$Script_Name][$Script_Version][$timestamp] - $1" echo "$message" if [[ "$ENABLE_LOGGING" == "true" ]]; then echo "$message" >> "$LOG_FILE" fi } #################################################################################################### # Validate / install swiftDialog #################################################################################################### function dialogInstall() { dialogURL=$(curl -L --silent --fail "https://api.github.com/repos/swiftDialog/swiftDialog/releases/latest" | awk -F '"' "/browser_download_url/ && /pkg\"/ { print \$4; exit }") expectedDialogTeamID="PWA5E9TQ59" log_message "Installing swiftDialog...." workDirectory=$( /usr/bin/basename "$0" ) tempDirectory=$( /usr/bin/mktemp -d "/private/tmp/$workDirectory.XXXXXX" ) /usr/bin/curl --location --silent "$dialogURL" -o "$tempDirectory/Dialog.pkg" teamID=$(/usr/sbin/spctl -a -vv -t install "$tempDirectory/Dialog.pkg" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()') if [[ "$expectedDialogTeamID" == "$teamID" ]]; then /usr/sbin/installer -pkg "$tempDirectory/Dialog.pkg" -target / sleep 2 dialogVersion=$( /usr/local/bin/dialog --version ) log_message "swiftDialog version ${dialogVersion} installed; proceeding...." else osascript -e 'display dialog "Please advise your Support Representative of the following error:\r\r• Dialog Team ID verification failed\r\r" with title "Dialog Missing: Error" buttons {"Close"} with icon caution' & exit 0 fi /bin/rm -Rf "$tempDirectory" } function dialogCheck() { if [[ "$USE_OSASCRIPT" == "true" ]]; then log_message "OSASCRIPT mode enabled, skipping swiftDialog check." return fi if [ ! -e "/Library/Application Support/Dialog/Dialog.app" ]; then log_message "swiftDialog not found. Installing...." dialogInstall else dialogVersion=$(/usr/local/bin/dialog --version) if [[ "${dialogVersion}" < "${swiftDialogMinimumRequiredVersion}" ]]; then log_message "swiftDialog version ${dialogVersion} found but swiftDialog ${swiftDialogMinimumRequiredVersion} or newer is required; updating...." dialogInstall else log_message "swiftDialog version ${dialogVersion} found; proceeding...." fi fi } dialogCheck # Create log file only if logging is enabled if [[ "$ENABLE_LOGGING" == "true" ]]; then touch "$LOG_FILE" fi log_message "Script started" # Smartcard Attribute Mapping for Local Accounts # Check for logged in user. currentUser="$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ && ! /loginwindow/ { print $3 }' )" log_message "Current user: $currentUser" if [[ -z "$currentUser" || "$currentUser" == "loginwindow" ]]; then log_message "No user is currently logged in. Exiting." exit 1 fi DIALOG_PATH="/usr/local/bin/dialog" # Architecture and user info arch=$(arch) AUID="$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ && ! /loginwindow/ { print $3 }' )" AUID_UID=$(id -u $AUID 2>/dev/null) currentUser="$AUID" # Check for pairing checkForPaired (){ log_message "Checking for existing smartcard pairing" tokenCheck=$(/usr/bin/dscl . read /Users/"$AUID" AuthenticationAuthority | grep -c tokenidentity) if [[ "$tokenCheck" > 0 ]]; then log_message "Unpairing smartcard from $AUID" /usr/sbin/sc_auth unpair -u "$AUID" log_message "Smartcard unpaired successfully" else log_message "No existing smartcard pairing found" fi } # Prompt the user to insert card, once inserted prompt will go away. prompt (){ log_message "Checking for inserted smartcard" if [[ $(launchctl asuser $AUID_UID security list-smartcards 2>/dev/null | grep -c com.apple.pivtoken ) -ge 1 ]]; then log_message "Smartcard already inserted" return 0 fi log_message "Displaying smartcard insertion prompt" if [[ "$USE_OSASCRIPT" == "true" ]]; then prompt_message="Please insert CAC before proceeding" while true; do button=$(osascript -e "display dialog \"$prompt_message\" with title \"Smartcard Mapping\" buttons {\"Cancel\", \"Proceed\"} default button \"Proceed\" with icon caution" 2>&1) if [[ $? -ne 0 ]] || [[ "$button" == *"Cancel"* ]]; then log_message "User cancelled smartcard prompt - exiting script" exit 0 fi if [[ $(launchctl asuser $AUID_UID security list-smartcards 2>/dev/null | grep -c com.apple.pivtoken ) -ge 1 ]]; then break fi prompt_message="CAC not detected, please insert CAC" done else "$DIALOG_PATH" \ --title "Smartcard Mapping" \ --messagealignment center \ --message "Please insert your smartcard to begin." \ --hideicon \ --button1text "Cancel" \ --buttonstyle center \ --progress 0 \ --small \ --ontop \ --moveable \ --height 200 \ --commandfile /var/tmp/dialog.log \ --position center 2> /dev/null & DIALOG_PID=$! while [[ $(launchctl asuser $AUID_UID security list-smartcards 2>/dev/null | grep -c com.apple.pivtoken ) -lt 1 ]]; do if ! kill -0 $DIALOG_PID 2>/dev/null; then log_message "Dialog window closed by user - exiting script" exit 0 fi sleep 1 done /bin/echo "quit:" >> /var/tmp/dialog.log fi log_message "Smartcard detected" } getUPN(){ log_message "Beginning UPN extraction process" # Create temporary directory to export certs: tmpdir=$(/usr/bin/mktemp -d) log_message "Created temporary directory: $tmpdir" # Export certs on smartcard to temporary directory: launchctl asuser $AUID_UID /usr/bin/security export-smartcard -e "$tmpdir" log_message "Certificates exported to temporary directory: $tmpdir" # Get path to Certificate for PIV Authentication: piv_path=$(ls "$tmpdir" | /usr/bin/grep '^Certificate For PIV') log_message "PIV certificate path: $tmpdir/$piv_path" # Get User Principle Name from Certificate for PIV Authentication: UPN="$(/usr/bin/openssl asn1parse -i -dump -in "$tmpdir/$piv_path" -strparse $(/usr/bin/openssl asn1parse -i -dump -in "$tmpdir/$piv_path" | /usr/bin/awk -F ':' '/X509v3 Subject Alternative Name/ {getline; print $1}') | /usr/bin/awk -F ':' '/UTF8STRING/{print $4}')" log_message "Retrieved UPN: $UPN" # Clean up the temporary directory /bin/rm -rf $tmpdir log_message "Cleaned up temporary directory: $tmpdir" } createAltSecId (){ log_message "Checking existing AltSecurityIdentities" altSecCheck=$(/usr/bin/dscl . -read /Users/"$AUID" AltSecurityIdentities 2>/dev/null | sed -n 's/.*Kerberos:\([^ ]*\).*/\1/p') log_message "Current AltSecurityIdentities value: $altSecCheck" if [[ "$UPN" = "" ]]; then log_message "Error: No UPN found for $AUID" if [[ "$USE_OSASCRIPT" == "true" ]]; then osascript -e 'display dialog "No UPN found on smartcard" with title "Smartcard Mapping" buttons {"Quit"} default button "Quit" with icon caution' else "$DIALOG_PATH" \ --title "Smartcard Mapping" \ --messagealignment center \ --message "No UPN found on smartcard" \ --hideicon \ --small \ --ontop \ --moveable \ --buttonstyle center \ --height 200 \ --button1text "Quit" 2> /dev/null fi elif [[ "$altSecCheck" = "$UPN" ]]; then log_message "AltSecurityIdentities already set to $UPN for $AUID" if [[ "$USE_OSASCRIPT" == "true" ]]; then osascript -e "display dialog \"Smartcard mapping was already set to $UPN\" with title \"Smartcard Mapping\" buttons {\"Quit\"} default button \"Quit\" with icon note" else "$DIALOG_PATH" \ --title "Smartcard Mapping" \ --messagealignment center \ --message "Smartcard mapping was already set to
$UPN" \ --hideicon \ --ontop \ --moveable \ --small \ --buttonstyle center \ --height 200 \ --button1text "Quit" 2> /dev/null fi else log_message "Adding $UPN to AltSecurityIdentities for $AUID" /usr/bin/dscl . -append /Users/"$AUID" AltSecurityIdentities Kerberos:"$UPN" log_message "Successfully added $UPN to AltSecurityIdentities for $AUID" if [[ "$USE_OSASCRIPT" == "true" ]]; then osascript -e "display dialog \"Successfully added $UPN to $AUID\" with title \"Smartcard Mapping\" buttons {\"Quit\"} default button \"Quit\" with icon note" else "$DIALOG_PATH" \ --title "Smartcard Mapping" \ --messagealignment center \ --message "Successfully added $UPN to $AUID" \ --hideicon \ --ontop \ --moveable \ --small \ --buttonstyle center \ --height 200 \ --button1text "Quit" 2> /dev/null fi fi } createMapping (){ log_message "Setting up SmartcardLogin.plist" if [ -f /etc/SmartcardLogin.plist ]; then log_message "SmartcardLogin.plist already exists" log_message "Removing existing SmartcardLogin.plist" rm -f /etc/SmartcardLogin.plist log_message "Removed existing SmartcardLogin.plist" fi trusted_authorities_entries=() if [[ "$INCLUDE_TRUSTED_AUTHORITIES" == "true" ]]; then log_message "Including DoD Trusted Authorities in SmartcardLogin.plist" TMPDIR=$(mktemp -d) ZIPURL="https://dl.dod.cyber.mil/wp-content/uploads/pki-pke/zip/unclass-dod_approved_external_pkis_trust_chains.zip" ZIPFILE="$TMPDIR/unclass-dod_approved_external_pkis_trust_chains.zip" curl -L --silent "$ZIPURL" -o "$ZIPFILE" unzip -q "$ZIPFILE" -d "$TMPDIR" base_dir=$(find "$TMPDIR" -maxdepth 1 -type d -name 'DoD_Approved_External_PKIs_Trust_Chains*' | head -n 1) cert_dir1="$base_dir/_DoD/Intermediate_and_Issuing_CA_Certs" cert_dir2="$base_dir/_DoD/Trust_Anchors_Self-Signed" fingerprints=() find "$cert_dir1" -type f -name '*.cer' -print0 > "$TMPDIR/certs1.list" find "$cert_dir2" -type f -name '*.cer' -print0 > "$TMPDIR/certs2.list" cat "$TMPDIR/certs1.list" "$TMPDIR/certs2.list" > "$TMPDIR/allcerts.list" while IFS= read -r -d '' cert; do fp=$(openssl x509 -in "$cert" -noout -fingerprint -sha256 2>/dev/null) if [[ -z "$fp" ]]; then fp=$(openssl x509 -in "$cert" -inform der -noout -fingerprint -sha256 2>/dev/null) fi fp=$(echo "$fp" | sed 's/^.*Fingerprint=//' | tr -d ':' | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]') if [[ -n "$fp" ]]; then trusted_authorities_entries+=(" $fp") fi done < "$TMPDIR/allcerts.list" rm -rf "$TMPDIR" else trusted_authorities_entries=(" ") fi log_message "Creating /etc/SmartcardLogin.plist" /bin/cat > "/etc/SmartcardLogin.plist" < AttributeMapping fields NT Principal Name formatString Kerberos:\$1 dsAttributeString dsAttrTypeStandard:AltSecurityIdentities TrustedAuthorities $(printf "%s\n" "${trusted_authorities_entries[@]}") NotEnforcedGroup EXEMPT_GROUP EOF log_message "SmartcardLogin.plist created successfully" } uninstall() { log_message "Starting uninstallation process" # Remove AltSecurityIdentities if /usr/bin/dscl . -read /Users/"$AUID" AltSecurityIdentities &>/dev/null; then log_message "Removing AltSecurityIdentities for $AUID" /usr/bin/dscl . -delete /Users/"$AUID" AltSecurityIdentities log_message "AltSecurityIdentities removed successfully" else log_message "No AltSecurityIdentities found for $AUID" fi # Remove SmartcardLogin.plist if [ -f /etc/SmartcardLogin.plist ]; then log_message "Removing SmartcardLogin.plist" rm -f /etc/SmartcardLogin.plist log_message "SmartcardLogin.plist removed successfully" else log_message "SmartcardLogin.plist not found" fi if [[ "$USE_OSASCRIPT" == "true" ]]; then osascript -e "display dialog \"Smartcard mapping has been removed for $AUID\" with title \"Smartcard Mapping\" buttons {\"Quit\"} default button \"Quit\" with icon note" else "$DIALOG_PATH" \ --title "Smartcard Mapping" \ --messagealignment center \ --message "Smartcard mapping has been removed for $AUID" \ --hideicon \ --ontop \ --moveable \ --small \ --buttonstyle center \ --height 200 \ --button1text "Quit" 2> /dev/null fi log_message "Uninstallation completed" } enableFileVault () { log_message "Enabling FileVault for $AUID" user_uuid=$(dscl . -read /Users/$AUID GeneratedUID 2>/dev/null | awk '{print $2}') log_message "User UUID for $AUID: $user_uuid" if [[ -z "$user_uuid" ]]; then log_message "Could not retrieve UUID for $AUID" return 1 fi # Check if user is already a FileVault enabled user if fdesetup list | grep -q "$user_uuid"; then log_message "FileVault is already enabled for $AUID (UUID: $user_uuid)" else log_message "User $AUID is not currently a FileVault enabled user" fi hash=$(sc_auth identities | awk '/PIV/ {print $1}') log_message "sc_auth hash for $AUID: $hash" if [[ -n "$hash" && -n "$AUID_UID" ]]; then # Only proceed if ;amidentity;$hash is not already present if ! dscl . -read /Users/$AUID AuthenticationAuthority 2>/dev/null | grep -q ";amidentity;$hash"; then log_message "Enabling FileVault with sc_auth for $AUID" launchctl asuser $AUID_UID sudo -u $AUID sc_auth filevault -o enable -u $AUID -h $hash log_message "FileVault enabled for $AUID with hash $hash" dscl . -append /Users/$AUID AuthenticationAuthority ";amidentity;$hash" log_message "Appended ;amidentity;$hash to AuthenticationAuthority for $AUID" diskutil apfs updatePreboot / >/dev/null 2>&1 log_message "Updated APFS preboot for $AUID" log_message "FileVault enabled for $AUID" else log_message "AuthenticationAuthority already contains ;amidentity;$hash for $AUID" fi else log_message "Could not enable FileVault: missing hash or UID" fi } # Main execution if [[ "$UNINSTALL" == "true" ]]; then uninstall else prompt checkForPaired getUPN if [[ $arch == "arm64" ]]; then enableFileVault fi createAltSecId createMapping # Remove any existing values for SmartCardEnforcement dscl . -delete /Users/$AUID SmartCardEnforcement 2>/dev/null fi log_message "Script completed successfully"