Delete duplicate AD & WiFI certificates

Gonzalo
New Contributor III

Hello,
Is there a way to delete duplicate certificates? - Today we push out our AD & Wifi certificate with a configuration profile, and had to do some minor changes on the settings for the certificate.

8e93f57b57644f2aa184f33e11b84cb6

The result is we now have two certificates with exactly same name $SERIALNUMBER.Domain.Companyname.com
and we would have to delete the older one, something like "...delete certificates created before December 16th.."

Is there any "smooth"" way to do this?

1 ACCEPTED SOLUTION

Gonzalo
New Contributor III

Following script worked like charm for me!
Thank you @AdamMartin

#!/bin/bash
# This script will remove all instances of a system keychain cert where: 
# 1) The certificate subject matches the cert subject below. 
# 2) It does not have the latest expiration date.
certSubject="Common-text-here i.e domain name"
certList=$( security find-certificate -c "${certSubject}" -p -a )
# echo "$certList"
# exit
# Get each cert into an array element
# Remove spaces
certList=$( echo "$certList" | sed 's/ //g' )
# Put a space after the end of each cert
certList=$( echo "$certList" | sed 's/-----ENDCERTIFICATE-----/-----ENDCERTIFICATE----- /g' )
# echo "$certList"
OIFS="$IFS"
IFS=' '
# read -a certArray <<< "${certList}"
declare -a certArray=($certList)
IFS="$OIFS"
i=-1
dateHashList=''
# Print what we got...
for cert in "${certArray[@]}"; do 
  let "i++"
  echo '---------'
  #   echo "$cert"
  #   echo '--'
  # Fix the begin/end certificate
  cert=$( echo "$cert" | sed 's/-----BEGINCERTIFICATE-----/-----BEGIN CERTIFICATE-----/g' )
  cert=$( echo "$cert" | sed 's/-----ENDCERTIFICATE-----/-----END CERTIFICATE-----/g' )
  #   echo "$cert"
  #   echo "$cert" | openssl x509 -text
  certMD5=$( echo "$cert" | openssl x509 -noout -fingerprint -sha1 -inform pem | cut -d "=" -f 2 | sed 's/://g' )
  certDate=$( echo "$cert" | openssl x509 -text | grep 'Not After' | sed -E 's|.*Not After : ||' )
  certDateFormatted=`date -jf "%b %d %T %Y %Z" "${certDate}" +%Y%m%d%H%M%S`
  echo "Cert ${i} : ${certDate} => $certDateFormatted"
  echo "Cert ${i} : ${certMD5}"
  NL=$'
'
  dateHashList="${dateHashList}${NL}${certDateFormatted} ${certMD5}"
done
echo
dateHashList=$( echo "$dateHashList" | sort | uniq )
lines=$( echo "$dateHashList" | wc -l | tr -d ' ' )
let "lines--"
echo "[info] There are $lines lines in the certificate date-hash list."
echo
i=0
OIFS="$IFS"
IFS=$'
'       # make newlines the only separator
for dateHash in $dateHashList; do
  let "i++"
  dateNum="${dateHash%% *}"
  hash="${dateHash##* }"
  echo "${i}| Hash : "$hash" | dateNum : "$dateNum""
  if [[ i -ne $lines ]]; then
    echo "=> This cert will be removed"
sudo security delete-certificate -Z $hash /Library/Keychains/System.keychain
    echo
  else
    echo "=> This cert will not be touched because it has the latest expiration date."
  fi
done
IFS="$OIFS"
exit 0

View solution in original post

7 REPLIES 7

Gonzalo
New Contributor III

Following script worked like charm for me!
Thank you @AdamMartin

#!/bin/bash
# This script will remove all instances of a system keychain cert where: 
# 1) The certificate subject matches the cert subject below. 
# 2) It does not have the latest expiration date.
certSubject="Common-text-here i.e domain name"
certList=$( security find-certificate -c "${certSubject}" -p -a )
# echo "$certList"
# exit
# Get each cert into an array element
# Remove spaces
certList=$( echo "$certList" | sed 's/ //g' )
# Put a space after the end of each cert
certList=$( echo "$certList" | sed 's/-----ENDCERTIFICATE-----/-----ENDCERTIFICATE----- /g' )
# echo "$certList"
OIFS="$IFS"
IFS=' '
# read -a certArray <<< "${certList}"
declare -a certArray=($certList)
IFS="$OIFS"
i=-1
dateHashList=''
# Print what we got...
for cert in "${certArray[@]}"; do 
  let "i++"
  echo '---------'
  #   echo "$cert"
  #   echo '--'
  # Fix the begin/end certificate
  cert=$( echo "$cert" | sed 's/-----BEGINCERTIFICATE-----/-----BEGIN CERTIFICATE-----/g' )
  cert=$( echo "$cert" | sed 's/-----ENDCERTIFICATE-----/-----END CERTIFICATE-----/g' )
  #   echo "$cert"
  #   echo "$cert" | openssl x509 -text
  certMD5=$( echo "$cert" | openssl x509 -noout -fingerprint -sha1 -inform pem | cut -d "=" -f 2 | sed 's/://g' )
  certDate=$( echo "$cert" | openssl x509 -text | grep 'Not After' | sed -E 's|.*Not After : ||' )
  certDateFormatted=`date -jf "%b %d %T %Y %Z" "${certDate}" +%Y%m%d%H%M%S`
  echo "Cert ${i} : ${certDate} => $certDateFormatted"
  echo "Cert ${i} : ${certMD5}"
  NL=$'
'
  dateHashList="${dateHashList}${NL}${certDateFormatted} ${certMD5}"
done
echo
dateHashList=$( echo "$dateHashList" | sort | uniq )
lines=$( echo "$dateHashList" | wc -l | tr -d ' ' )
let "lines--"
echo "[info] There are $lines lines in the certificate date-hash list."
echo
i=0
OIFS="$IFS"
IFS=$'
'       # make newlines the only separator
for dateHash in $dateHashList; do
  let "i++"
  dateNum="${dateHash%% *}"
  hash="${dateHash##* }"
  echo "${i}| Hash : "$hash" | dateNum : "$dateNum""
  if [[ i -ne $lines ]]; then
    echo "=> This cert will be removed"
sudo security delete-certificate -Z $hash /Library/Keychains/System.keychain
    echo
  else
    echo "=> This cert will not be touched because it has the latest expiration date."
  fi
done
IFS="$OIFS"
exit 0

View solution in original post

friveraLC
New Contributor III

Thanks for this script! We issue certs to the serial number of the device. Is there a way to put in a variable for the certSubject? I would like to make that the Serial Number of the device. Thank you again! This script is awesome! @Gonzalo @AdamMartin

hinrichd
New Contributor III

This script is awesome. How do you scope this policy? Is there a way a build a Smart Group with clients having duplicated certificates? Actually I found only a way to make a smart group with clients that have installed a common certificate but can not limit this to duplicates.

Nmangal
New Contributor III

Thank you for sharing the script @Gonzalo .
It works like a charm.

Gonzalo
New Contributor III

@colorenz shared a updated version of this script, that works better if your certificate has the same name as the computer.

ComputerName=$(/usr/sbin/scutil --get ComputerName)
#LogFile
LogFile="/var/log/adcs-certificates.log"
#Log Function
logger() {
    /bin/echo $(date "+%Y-%m-%d %H:%M:%S ") $1 >>"${LogFile}"
    /bin/echo $(date "+%Y-%m-%d %H:%M:%S ") $1
}
logger "-------------------------------------"
logger "Start: Check for mutilpe Certificates"
certList=$(security find-certificate -c $ComputerName -p -a)
# Get each cert into an array element
# Remove spaces
certList=$(echo "$certList" | sed 's/ //g')
# Put a space after the end of each cert
certList=$(echo "$certList" | sed 's/-----ENDCERTIFICATE-----/-----ENDCERTIFICATE----- /g')
# echo "$certList"
OIFS="$IFS"
IFS=' '
# read -a certArray <<< "${certList}"
declare -a certArray=($certList)
IFS="$OIFS"
i=-1
dateHashList=''
# Print what we got...
for cert in "${certArray[@]}"; do
    let "i++"
    # Fix the begin/end certificate
    cert=$(echo "$cert" | sed 's/-----BEGINCERTIFICATE-----/-----BEGIN CERTIFICATE-----/g')
    cert=$(echo "$cert" | sed 's/-----ENDCERTIFICATE-----/-----END CERTIFICATE-----/g')
    #   echo "$cert"
    #   echo "$cert" | openssl x509 -text
    certMD5=$(echo "$cert" | openssl x509 -noout -fingerprint -sha1 -inform pem | cut -d "=" -f 2 | sed 's/://g')
    certDate=$(echo "$cert" | openssl x509 -text | grep 'Not After' | sed -E 's|.*Not After : ||')
    certDateFormatted=$(date -jf "%b %d %T %Y %Z" "${certDate}" +%Y%m%d%H%M%S)
    logger "Cert ${i} : ${certDate} => $certDateFormatted"
    logger "Cert ${i} : ${certMD5}"
    NL=$'
'
    dateHashList="${dateHashList}${NL}${certDateFormatted} ${certMD5}"
done
dateHashList=$(echo "$dateHashList" | sort | uniq)
lines=$(echo "$dateHashList" | wc -l | tr -d ' ')
let "lines--"
logger "Info There are $lines lines in the certificate date-hash list."
i=0
OIFS="$IFS"
IFS=$'
' # make newlines the only separator
for dateHash in $dateHashList; do
    let "i++"
    dateNum="${dateHash%% *}"
    hash="${dateHash##* }"
    logger "${i}| Hash : "$hash" | dateNum : "$dateNum""
    if [[ i -ne $lines ]]; then
        logger "=> This cert will be removed"
        sudo security delete-certificate -Z $hash /Library/Keychains/System.keychain
        logger "=> Cert was $hash removed"
    else
        logger "=> This cert will not be touched because it has the latest expiration date."
    fi
done
IFS="$OIFS"
logger "-------------------------------------"
logger "End: Check for mutilpe Certificates"

Use it together with this EA and create a smart group.
6a7a401a0607410eb7763c7de2b52dd9

#!/bin/sh
#Get current number of Computer Certificate
#Current ComputerName
ComputerName=$(/usr/sbin/scutil --get ComputerName)
ComputerCertificateCount=$(/usr/bin/security find-certificate -a -c $ComputerName -p -Z "/Library/Keychains/System.keychain" | grep SHA-1 | wc | awk '{print $1}')
echo "ComputerCertificateCount"
echo "<result>$ComputerCertificateCount</result>"

Once again, thanks @colorenz

anthonybrw
New Contributor

This updated script resolved a lot of headache in our environment @Gonzalo @colorenz, kudos! We've also seen instances where the duplicate certs match their username - any suggestions for how to phrase the first line so that this script searches for username?

jhalvorson
Valued Contributor

Use with caution.  The script looks a lot like one provided to me by Jamf support.  I've recently discovered that it does review all certs based on computer name and keeps the one with the longest expire date.  Unfortunately, a few Macs have had certs that have the name of the computer, but are self-assigned or issued by things other than our ADCS services.  One I stumbled on was a cert that was self-assigned by CUPS.  The result is that every ADCS PKI cert was removed but the Self-Assigned remained in place because it had the longest time to expire.