Getting Expiry date of Certificate

cindySingh
New Contributor III

Hi, Has anyone ever tried to pull the expiry date of any Certificate? I can see it is in my Keychain and also can see the last date there. But I need to find a way to pull report from all Macs and send out notification based on expiry date.
Unable to pull this info. Any ideas?

1 ACCEPTED SOLUTION

Chris
Valued Contributor

You can use the security command to find the cert in the keychain and pipe the output into openssl.
Are you looking for a user- or system-cert?
For a cert in the System keychain you can do something like this in an extension attribute:

#!/bin/sh

certexpdate=$(/usr/bin/security find-certificate -a -c "name_of_your_cert" -p -Z "/Library/Keychains/System.keychain" | /usr/bin/openssl x509 -noout -enddate| cut -f2 -d=)

dateformat=$(/bin/date -j -f "%b %d %T %Y %Z" "$certexpdate" "+%Y-%m-%d %H:%M:%S")

echo "<result>$dateformat</result>"

View solution in original post

33 REPLIES 33

Chris
Valued Contributor

You can use the security command to find the cert in the keychain and pipe the output into openssl.
Are you looking for a user- or system-cert?
For a cert in the System keychain you can do something like this in an extension attribute:

#!/bin/sh

certexpdate=$(/usr/bin/security find-certificate -a -c "name_of_your_cert" -p -Z "/Library/Keychains/System.keychain" | /usr/bin/openssl x509 -noout -enddate| cut -f2 -d=)

dateformat=$(/bin/date -j -f "%b %d %T %Y %Z" "$certexpdate" "+%Y-%m-%d %H:%M:%S")

echo "<result>$dateformat</result>"

cindySingh
New Contributor III

damn you're awesome !!! It took me long but couldnt reach here.
c746224e71a24827ab5c668961c471a6
thanks, so much :)

globaldominatio
New Contributor II

This was cool, no doubt this JAMF community is best. Saves so much of time for lazy people like myself <*.->

Chris
Valued Contributor

No problem.
You might run into this issue
later on, so feel free to vote it up ;)

May
Contributor III

Hi @Chris

This is really useful, thanks!

Do you know how i could change it to retreive the expiry date of a certificate within the users keychain ?
i've tried as below but get an error, i've had a dig on the openssl options but can't figure the correct way.

#!/bin/sh
username=$( stat -f%Su /dev/console )
echo "$username"

if [ $username = "root" ]; then

#echo "Non AD user logged in- $username - stopping script"
    exit

else

cert_name="OURCERT.ourcompany.com"
desired_keychain="/Users/$username/Library/Keychains/login.keychain"

certexpdate=$(/usr/bin/security find-certificate -a -c "$cert_name" -p -Z "desired_keychain" | /usr/bin/openssl x509 -noout -enddate| cut -f2 -d=)

dateformat=$(/bin/date -j -f "%b %d %T %Y %Z" "$certexpdate" "+%Y-%m-%d %H:%M:%S")

echo "<result>$dateformat</result>"

fi
90767:error:0906D06C:PEM routines:PEM_read_bio:no start line:/SourceCache/OpenSSL098/OpenSSL098-52.8.3/src/crypto/pem/pem_lib.c:648:Expecting: TRUSTED CERTIFICATE Failed conversion of '' using format%b %d %T %Y %Z'' date: illegal time format usage: date [-jnu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ... [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format] <result></result>

Thank you

mm2270
Legendary Contributor III

Hey @May You might want to read through this thread.
https://jamfnation.jamfsoftware.com/discussion.html?id=9656
I believe you're running into the issue outlined in the later parts of the thread, having to do with the difference in the timezone. Since the cert timestamp gets recorded in GMT, regardless of where you're located, date has an issue with converting time from the recorded format to another one because your Mac isn't located in the same GMT time zone. Is it stupid? Yep, it is, but that's apparently how it works, though I consider it a flaw if not an actual bug.
On the thread, @erikberglund offers a fix for this issue.

May
Contributor III

@mm2270 I tried with the format that @erikberglund suggests but i couldn't get it to work,
though it did help me notice the missing $ before desired_keychain, Thank you!

so this works for pulling the date expiry date from a users certificate

#!/bin/sh

username=$( stat -f%Su /dev/console )

if [ $username = "root" ]; then

#echo "Non AD user logged in- $username - stopping script"
    exit

else

cert_name="OURCERT.ourcompany.com"
desired_keychain="/Users/$username/Library/Keychains/login.keychain"


certexpdate=$(/usr/bin/security find-certificate -a -c "$cert_name" -p -Z "$desired_keychain" | /usr/bin/openssl x509 -noout -enddate | cut -f2 -d=)

dateformat=$(/bin/date -j -f "%b %d %T %Y %Z" "$certexpdate" "+%Y-%m-%d %H:%M:%S")

echo "<result>$dateformat</result>"

fi

and i've got this one together to see if it expires within an amount of days (please excuse my scripting..:)

#!/bin/sh
username=$( stat -f%Su /dev/console )

if [ $username = "root" ]; then

#echo "Non AD user logged in- $username - stopping script"
    exit

else

cert_name="OURCERT.ourcompany.com"
desired_keychain="/Users/$username/Library/Keychains/login.keychain"

expires="2419200"

#1  days = 86400
#7  days = 604800
#14 days = 1209600
#21 days = 1814400
#28 days = 2419200

certexpdate=$(/usr/bin/security find-certificate -a -c "$cert_name" -p -Z "$desired_keychain" | /usr/bin/openssl x509 -checkend "$expires")

echo "$certexpdate in under 28 days - $cert_name" 

fi

CAJensen01
Contributor

This works fine if there's only a single cert matching that name. If there are multiples, it will only pull the date from the first.

Anyone have any ideas how you could get the expiry date for multiple certs by the same name, and then determine which is the newest?

bentoms
Release Candidate Programs Tester

@CAJensen01 can you expand upon why you're looking at this?

Typically multiple copies of the same cert are not a problem, if an expired cert is in the keychain then it shouldn't be used. Instead the non-expired one's should be by the OS

CAJensen01
Contributor

What I'm looking for @bentoms, is if I have multiple certs with the same name and different expiry dates, the logic to locate and select the one with the expiry date the furthest in the future.

When associating that cert with an application (creating an identity preference in the keychain), it is ideal to select the one that is furthest out into the future, so that if (for example) the default one selected expires in only 3 months, the script doesn't create the identity preference based off of that certificate, but rather, the one that will impact the users the furthest into the future. I haven't quite found the logic to do this, but haven't spent a ton of time trying, either.

alexjdale
Valued Contributor III

Being able to select a single specific cert if you have multiples of the same name is something I'd like to see. I can delete a certificate by providing the hash, why can't I view one the same way?

I'd really like to be able to clean up old certs as well as certs issued by our older SHA1 infrastructure without trying to parse a giant dump of cert data.

roiegat
Contributor III

Quick question....so we have certs that have the same name "name.company.com" so I have a script that looks for that, but so far it only finds the first one. We have issues where one of them is expired, so we need to renew it.

Also, in conditions that there a multiple certs with the same name, is there a way to delete all but one of them via script?

Key1
New Contributor III

@CAJensen01 I used the cert beginning text to count the matches

#!/bin/sh
Certs = $( security find-certificates -a -c <NAME_OF_CERT>  -p <KEYCHAIN PATH> | grep -c -e '-----BEGIN CERTIFICATE-----' )

if  [[ $Certs -gt 1 ]]; then
echo "More than one cert found!"
fi

@roiegat you can parse the Certs into an array using the same tags on the cert

#!/bin/sh
IFS=","
CertList= $( security find-certificates -a -c <NAME_OF_CERT>  -p <KEYCHAIN PATH> | sed s/"-----END CERTIFICATE-----"/"-----END CERTIFICATE-----,"/g)
Array=($CertList)
for ACert in "${Array[@]}"
do
#Do date check on ACert variable and remove if expired
#Recount certs etc etc
done

roiegat
Contributor III

@Key1 Great info!

Got one more trick question, is there way to script the capability to have the cert be allowed to use by an application? So currently in Casper we set it up so all applications can use it, but ideally we'd like just our VPN app to access it. So always looking for a scripting method to limit it. But priority is to remove duplicates first...so this is a great start.

Key1
New Contributor III

@roiegat To do it manually you select a cert that has a private key, double click the key and add the app to the access control list.

Not something I've done by script before but looking at the man page for security: [https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/security.1.html](link URL)

it looks like you can import an item and set its application access control list at that time with -T

import inputfile [-k keychain] [-t type] [-f format] [-w] [-P passphrase] [options...] Import one or more items from inputfile into a keychain. If keychain isn't provided, items will be imported into the user's default keychain. Options: -k keychain Specify keychain into which item(s) will be imported. -t type Specify the type of items to import. Possible types are cert, pub, priv, ses-sion, session, sion, cert, and agg. Pub, priv, and session refer to keys; agg is one of the aggregate types (pkcs12 and PEM sequence). The command can often figure out what item_type an item contains based in the filename and/or item_format. -f format Specify the format of the exported data. Possible formats are openssl, bsafe, raw, pkcs7, pkcs8, pkcs12, x509, openssh1, openssh2, and pemseq. The command can often figure out what format an item is in based in the filename and/or item_type. -w Specify that private keys are wrapped and must be unwrapped on import. -x Specify that private keys are non-extractable after being imported. -P passphrase Specify the unwrapping passphrase immediately. The default is to obtain a secure passphrase via GUI. -a attrName attrValue Specify optional extended attribute name and value. Can be used multiple times. This is only valid when importing keys. -A Allow any application to access the imported key without warning (insecure, not recommended!) -T appPath Specify an application which may access the imported key (multiple -T options are allowed)

roiegat
Contributor III

So having issue with the script above....when I run it with my settings I get:
./cert.sh: line 17: -----END CERTIFICATE-----: command not found

Here's line 17:

CertList= $(/usr/bin/security find-certificate -a -c "$cert_name"  -p "$desired_keychain" | sed s/"-----BEGIN CERTIFICATE-----"/"-----END CERTIFICATE-----"/g)```


It seems like it doesn't like the comma at the end of END CERTIFICATE line, removing it spits out the certs and then gets the following error:

......
nuiA/9pZRktznpvy3dOrLKFJfsqoCu2E5MUxUll79DUF6rN/UIF4dDXUDYB3T5Ac
qLNuIS5yCvHquzBrkBF0XjqX1olQN0X5Whox8UgA3e0pKTO9htER9Q==
-----END CERTIFICATE-----: File name too long

So not sure what I'm doing wrong here. The first script works in determining there is more then one. Just trying to break them up now and checking the dates. ```

Key1
New Contributor III

@roiegat yup sorry my mistake shouldn't be BEGIN CERTIFICATE, i updated my sed command above, we are adding in a , after every END CERTIFICATE so we can use it for a delimiter for the array.

#!/bin/sh
CertList= $( security find-certificates -a -c <NAME_OF_CERT>  -p <KEYCHAIN PATH> | sed s/"-----END CERTIFICATE-----"/"-----END CERTIFICATE-----,"/g)

mm2270
Legendary Contributor III

You can list all installed certificates for a given keychain (or the System keychain if run as root and no keychain is specified) with the following command

/usr/bin/security find-certificate -a | awk -F'"' '/labl/{print $4}'

From there, you can use grep, awk or some other regex matching tool to grab only the certificates you care about. For example, using the example of name.company.com you posted above, it might be something like

/usr/bin/security find-certificate -a | awk -F'"' '/labl/{print $4}' | grep "^name.company.com$"

Using the ^ and $ start and end line indicators will ensure its only getting the certs with the exact name you specify and not any others that use the same string within their names.
As mentioned above, you could drop those into an array if you needed to do something to them later. Or just feed that output back to the script to loop over them.

roiegat
Contributor III

@Key1 Fixing that I still get:

bash-3.2$ sudo ./cert.sh
./cert.sh: line 9: -----BEGIN: command not found

When running the script.

@mm2270 You method worked well...but once I have all the names, when I run an expiration date check, it seems to only get the first one.

So back to more testing.

normanchan
New Contributor II

@mm2270 thank you for your snippet. would you know by any chance how I would grep a certificate name that's randomly generated by the JSS? For example -- after enrollment, a cert with common name "D50FDCBA-C7F1-4DB2-A9CA-46AB3A3A164B" gets generated along with a private key into keychain.

I have a couple of machines that have issues enrolling -- jamf.log states issue with device signature. I was able to replicate this in a virtual machine only after deleting the JSS certs, and also removing framework, I was then able to run a quickadd package for enrollment.

Any help would be greatly appreciated!

millersys_seth
New Contributor

Thanks to everyone who took the answer to this question as far as you did, it was definitely helpful - we hope this will be helpful to others as well.

We needed to enumerate only the AD issued certs in the System keychain as part of an 802.1x troubleshooting exercise. The script below, which will work standalone or via ARD, uses dsconfigad to get the bound computer's name, then returns ALL the certs that match the AD name in the System Keychain. It then passes those certs into a proper array; the array's values are then passed to openssl in a way that it will accept (probably the trickiest bit). We wanted the output to return the cert's subject, issuer, expiration date, and serial number, but you can season to taste with anything you like that openssl will give you.

Enjoy!

#!/bin/sh
# get the AD name and domain, make the FQDN of the machine to match the cert we're looking for
AD_NAME=$(dsconfigad -show | grep "Computer Account" | awk '{print $4}' | rev | cut -c 2- | rev)
# Not used, but may come in handy if we need to match on a different AD value later. 
#   AD_DOMAIN=$(dsconfigad -show | awk '/Active Directory Domain/{print $NF}')
#   FQDN="$AD_NAME.$AD_DOMAIN"
#
#We look through the entire system keychain for any Computer certificates that match the AD Name of the computer. 
#Then we load the entire certificate and SHA Hash for ALL matches into an array, which is important if you are trying to locate duplicate certificates. 
IFS=","
CertList=$(/usr/bin/security find-certificate -a -c $AD_NAME -p -Z "/Library/Keychains/System.keychain" | sed s/"-----END CERTIFICATE-----"/"-----END CERTIFICATE-----,"/g)
if [ -n "$CertList" ]; then
    Array=($CertList)
    for ACert in "${Array[@]}"
    do
#We pass each matching cert to openSSL, and pull a handful of useful details from each matched certificate.
    certsubject=$(/usr/bin/openssl x509 -noout -subject <<< $ACert)
    certissuer=$(/usr/bin/openssl x509 -noout -issuer <<< $ACert)
    certserial=$(/usr/bin/openssl x509 -noout -serial <<< $ACert)
    certexpdate=$(/usr/bin/openssl x509 -noout -enddate <<< $ACert | cut -f2 -d=)
    certexpdateformatted=$(/bin/date -j -f "%b %d %T %Y %Z" "$certexpdate" "+%Y-%m-%d %H:%M:%S")
    echo "$certsubject,$certissuer,$certexpdateformatted,$certserial"
    done
#Added this to differentiate between non-responsive systems and systems that just don't have a matching cert at all. 
#Esp useful in case you are running the contents from ARD (it works)
else
    echo "No Matching Certs Found"
    fi

Xopher
New Contributor III

@ millersys_seth
Thank you very much for posting. We were needing this very thing and I wanted to post version in case anyone found it helpful. Just added a bit to compare expiration date so we can count at least one machine CA entry valid- as we often see expired entries. Also edited out seconds in time; didn't think they were needed for our purpose. Seems to work but I haven't tested in Jamf Pro yet.

#!/bin/sh

# Get the AD name, domain and Date/Time, make the FQDN of the machine to match the cert we're looking for
AD_NAME=$(dsconfigad -show | grep "Computer Account" | awk '{print $4}' | rev | cut -c 2- | rev)
curr_Date=$(date "+%Y-%m-%d %H:%M")

# Not used, but may come in handy if we need to match on a different AD value later. 
   #AD_DOMAIN=$(dsconfigad -show | awk '/Active Directory Domain/{print $NF}')
   #FQDN="$AD_NAME.$AD_DOMAIN"

#Look through the entire system keychain for any Computer certificates that match the AD Name of the computer. 
#Then load the entire certificate and SHA Hash for ALL matches into an array, which is important if you are trying to locate duplicate certificates. 
IFS=","
CertList=$(/usr/bin/security find-certificate -a -c $AD_NAME -p -Z "/Library/Keychains/System.keychain" | sed s/"-----END CERTIFICATE-----"/"-----END CERTIFICATE-----,"/g)
if [ -n "$CertList" ]; then
    Array=($CertList)
    for ACert in "${Array[@]}"
    do
#Pass each matching cert to openSSL, and pull a handful of useful details from each matched certificate.
    certsubject=$(/usr/bin/openssl x509 -noout -subject <<< $ACert)
    certexpdate=$(/usr/bin/openssl x509 -noout -enddate <<< $ACert | cut -f2 -d=)
    certexpdateformatted=$(/bin/date -j -f "%b %d %T %Y %Z" "$certexpdate" "+%Y-%m-%d %H:%M")

# Next 3 not used, but may come in handy if we need to match on a different AD value later.    
    #certissuer=$(/usr/bin/openssl x509 -noout -issuer <<< $ACert)
    #certserial=$(/usr/bin/openssl x509 -noout -serial <<< $ACert)
    #echo "$certsubject,$certissuer,$certexpdateformatted,$certserial"

#Can comment out the next echo but good for testing
    echo "$certsubject,$certexpdateformatted"
    done
    fi

    if [ "$certexpdateformatted" > "$curr_Date" ]; then
    echo "Valid CA Found"

#Added this to differentiate between non-responsive systems and systems that just don't have a matching cert at all. 
#Esp useful in case you are running the contents from ARD (it works)
else
    echo "No Valid CA Found"
    fi

dstranathan
Valued Contributor II

@CAJensen01 @roiegat @millersys_seth @Key1

Just curious, What's the purpose of performing the sed regex find/replace operation on the target certificates like the snippet below:

| sed s/"-----END CERTIFICATE-----"/"-----END CERTIFICATE-----,"/g

Isnt...

CertList=$(/usr/bin/security find-certificate -a -c $AD_NAME -p -Z "/Library/Keychains/System.keychain")

...enough to get the certificate(s) you are looking for?

Or is adding the , (1 comma) in the sed find and replace operation just a way to display duplicates better (with IFS set to a , (1 comma)?

alexjdale
Valued Contributor III

I'm not going to post all the code right now, but if someone is interested I can put something together. Since we're coming at this from an 802.1x angle, I've found that reading /Library/Preferences/SystemConfiguration/com.apple.network.eapolclient.configuration.plist gives you a lot of information about your configured 802.1x networks. Combine this with reading the identities on the keychain, and you can get a full picture of which certs are in use, when they're going to expire, etc. and use that information to take action (like deleting certs that are not in use, instead of going by expiration dates).

I have a script that cycles through our networks and evaluates them, outputting info like this for each network:

WiFiSSID Network Configuration: Cert SHA-1: C7205B5F322B3572B9BE1C54AD16C0A25C87C8C3 Cert Common name: COMPUTERNAME.sample.com Common name matches this hostname Cert expires on Apr 29 19:54:53 2022 GMT TLS trust properly set for *.sample.com PASSED - 0 problem(s) found for WiFiSSID

Asri-Zainal
New Contributor II

Hi!
The information from this thread has helped me list out all the certs on a mac with its expiry information however it's very flakey when setting it up as a Jamf extension attribute.

Using the code below as an EA, i was able to get the cert info on some hosts but on others it was not able to and reporting "<result>No Matching Certs Found</result>" instead.

The odd thing is I'm able to get the cert information when running this script locally on the affected mac, Jamf just doesn't like it as an EA.

Any ideas?

#!/bin/sh
# get the AD name and domain, make the FQDN of the machine to match the cert we're looking for
AD_NAME=$(scutil --get ComputerName)
username=$( stat -f%Su /dev/console )
cert_name="name of cert"
desired_keychain="/Users/$username/Library/Keychains/login.keychain"
# Not used, but may come in handy if we need to match on a different AD value later. 
#   AD_DOMAIN=$(dsconfigad -show | awk '/Active Directory Domain/{print $NF}')
#   FQDN="$AD_NAME.$AD_DOMAIN"
#
#We look through the entire system keychain for any Computer certificates that match the AD Name of the computer. 
#Then we load the entire certificate and SHA Hash for ALL matches into an array, which is important if you are trying to locate duplicate certificates. 
IFS=","
CertList=$(/usr/bin/security find-certificate -a -c $cert_name -p | sed s/"-----END CERTIFICATE-----"/"-----END CERTIFICATE-----,"/g)
if [ -n "$CertList" ]; then
    Array=($CertList)
    for ACert in "${Array[@]}"
    do
#We pass each matching cert to openSSL, and pull a handful of useful details from each matched certificate.
    certsubject=$(/usr/bin/openssl x509 -noout -subject <<< $ACert)
    certissuer=$(/usr/bin/openssl x509 -noout -issuer <<< $ACert)
    certserial=$(/usr/bin/openssl x509 -noout -serial <<< $ACert)
    certexpdate=$(/usr/bin/openssl x509 -noout -enddate <<< $ACert | cut -f2 -d=)
    certexpdateformatted=$(/bin/date -j -f "%b %d %T %Y %Z" "$certexpdate" "+%Y-%m-%d %H:%M:%S")
    listResults+="Serial: $certserial | Expiry Date: $certexpdateformatted
"
    done
    echo "<result>$listResults</result>"
#Added this to differentiate between non-responsive systems and systems that just don't have a matching cert at all. 
#Esp useful in case you are running the contents from ARD (it works)
else
    echo "<result>No Matching Certs Found</result>"
    fi

Claudius
New Contributor II

Hi there,

I use the following script to search for AD certificates in the keychain management and display their expiration date in the extension attributes.
In our environment, we always need to renew the AD certificate so that our managed Macs can continue to access our university network via 802.1X.
Therefore, it is so that there is not only one AD certificate in the keychain management. The number keeps growing whenever a new certificate is created on a device.
With my script now I always want to know only the expiration date of the latest certificate.
Could someone please help me here.
Thank you very much.

 

#!/bin/bash
COMPUTER_NAME=$(scutil --get ComputerName)
EXPIRATION_DATE=$(/usr/bin/security find-certificate -a -c "$COMPUTER_NAME" -p -Z "/Library/Keychains/System.keychain" | /usr/bin/openssl x509 -noout -enddate| cut -f2 -d=)
EXPIRATION_DATE_FORMATTED=$(/bin/date -j -f "%b %d %T %Y %Z" "$EXPIRATION_DATE" "+%d.%m.%Y %H:%M:%S")
echo "<result>$EXPIRATION_DATE_FORMATTED</result>"

 

dstranathan
Valued Contributor II

In the past I have used logic like this example to get a total count of machine certs (or any cert really based on the criteria)  to help determine if I need to scrub older certs or not

#!/bin/zsh

COMPUTER_NAME=$( scutil --get ComputerName )
DOMAIN="my.domain."
CERTIFICATE_COUNT=$( security find-certificate -a -c "${COMPUTER_NAME}.${DOMAIN}" "/Library/Keychains/System.keychain" | awk -F'"' '/alis/{print $4}' | wc -l | awk '{$1=$1;print}' )
echo "<result>${CERTIFICATE_COUNT}</result>"

 

Thanks @dstranathan,
but I don't need the number of AD certificates, I need the expiration date of the most recent one.

Via

<result>$EXPIRATION_DATE_FORMATTED</result>

I would enter the information as extension attribute. So I can always see in the overview of all managed Macs when the currently valid certificate expires (when renewing a new certificate is issued, but the old one remains in the keyring management) and when it must be renewed at the latest.

Asri-Zainal
New Contributor II

Below's our EA that lists all certificates in the naming convention specified in 3 keychains: system, login & openvpn


Feel free to edit/remove parts that's not needed.

Since our desktops and portables has different naming convention, there's a logic that checks what type of hardware model and assign accordingly.

 

#!/bin/sh
# get the AD name and domain, make the FQDN of the machine to match the cert we're looking for
#AD_NAME=$(scutil --get ComputerName)
username=$( stat -f%Su /dev/console )
login_keychain="/Users/$username/Library/Keychains/login.keychain"
system_keychain="/Library/Keychains/System.keychain"
openvpn_keychain="/Users/$username/Library/Keychains/openvpn.keychain"

# Gets the Model Identifier, splits name and major version
MODELIDENTIFIER=$(/usr/sbin/sysctl -n hw.model)
MODELNAME=$(echo "$MODELIDENTIFIER" | sed 's/[^a-zA-Z]//g')

# wildcard search for certificates ending with *.company.org
  # FQDN assignment
  # Magically form hostname based on asset type: Desktops -> lon , Portables -> nomad
  lon_fqdn=".lon.company.org"
  nomad_fqdn=".nomad.company.org"

  case "$model_name" in
    iMac*|iMacPro*|Macmini*|MacPro*|Mac13,1*)
      cert_name="$lon_fqdn"
      ;;
    *)
      cert_name="$nomad_fqdn"
      ;;
  esac


# Not used, but may come in handy if we need to match on a different AD value later. 
#   AD_DOMAIN=$(dsconfigad -show | awk '/Active Directory Domain/{print $NF}')
#   FQDN="$AD_NAME.$AD_DOMAIN"
#
#We look through the entire system/login/OpenVPN keychain for any Computer certificates that matches 
#Then we load the entire certificate and SHA Hash for ALL matches into an array, which is important if you are trying to locate duplicate certificates. 
IFS=","

#System Keychain
CertList=$(/usr/bin/security find-certificate -a -c $cert_name -p -Z $system_keychain | sed s/"-----END CERTIFICATE-----"/"-----END CERTIFICATE-----,"/g)
if [ -n "$CertList" ]; then
    Array=($CertList)
    for ACert in "${Array[@]}"
    do
#We pass each matching cert to openSSL, and pull a handful of useful details from each matched certificate.
    certsubject=$(/usr/bin/openssl x509 -noout -subject <<< $ACert)
    certCN=$(/usr/bin/openssl x509 -noout -subject <<< $ACert | sed -n '/^subject/s/^.*CN=//p')
    certissuer=$(/usr/bin/openssl x509 -noout -issuer <<< $ACert)
    certserial=$(/usr/bin/openssl x509 -noout -serial <<< $ACert)
    certexpdate=$(/usr/bin/openssl x509 -noout -enddate <<< $ACert | cut -f2 -d=)
    certexpdateformatted=$(/bin/date -j -f "%b %d %T %Y %Z" "$certexpdate" "+%Y-%m-%d %H:%M:%S")
    systemResults+="System Keychain: $certCN | $certserial | Expiry Date: $certexpdateformatted\n"
    done
#Added this to differentiate between non-responsive systems and systems that just don't have a matching cert at all. 
#Esp useful in case you are running the contents from ARD (it works)
else
    systemResults="No Matching Certs Found in System Keychain\n"
fi

#Login Keychain
CertList=$(/usr/bin/security find-certificate -a -c $cert_name -p -Z $login_keychain | sed s/"-----END CERTIFICATE-----"/"-----END CERTIFICATE-----,"/g)
if [ -n "$CertList" ]; then
    Array=($CertList)
    for ACert in "${Array[@]}"
    do
#We pass each matching cert to openSSL, and pull a handful of useful details from each matched certificate.
    certsubject=$(/usr/bin/openssl x509 -noout -subject <<< $ACert)
    certCN=$(/usr/bin/openssl x509 -noout -subject <<< $ACert | sed -n '/^subject/s/^.*CN=//p')
    certissuer=$(/usr/bin/openssl x509 -noout -issuer <<< $ACert)
    certserial=$(/usr/bin/openssl x509 -noout -serial <<< $ACert)
    certexpdate=$(/usr/bin/openssl x509 -noout -enddate <<< $ACert | cut -f2 -d=)
    certexpdateformatted=$(/bin/date -j -f "%b %d %T %Y %Z" "$certexpdate" "+%Y-%m-%d %H:%M:%S")
    loginResults+="Login Keychain: $certCN | $certserial | Expiry Date: $certexpdateformatted\n"
    done
#Added this to differentiate between non-responsive systems and systems that just don't have a matching cert at all. 
#Esp useful in case you are running the contents from ARD (it works)
else
    loginResults="No Matching Certs Found in Login Keychain\n"
fi

#OpenVPN Keychain
CertList=$(/usr/bin/security find-certificate -a -c $cert_name -p -Z $openvpn_keychain | sed s/"-----END CERTIFICATE-----"/"-----END CERTIFICATE-----,"/g)
if [ -n "$CertList" ]; then
    Array=($CertList)
    for ACert in "${Array[@]}"
    do
#We pass each matching cert to openSSL, and pull a handful of useful details from each matched certificate.
    certsubject=$(/usr/bin/openssl x509 -noout -subject <<< $ACert)
    certCN=$(/usr/bin/openssl x509 -noout -subject <<< $ACert | sed -n '/^subject/s/^.*CN=//p')
    certissuer=$(/usr/bin/openssl x509 -noout -issuer <<< $ACert)
    certserial=$(/usr/bin/openssl x509 -noout -serial <<< $ACert)
    certexpdate=$(/usr/bin/openssl x509 -noout -enddate <<< $ACert | cut -f2 -d=)
    certexpdateformatted=$(/bin/date -j -f "%b %d %T %Y %Z" "$certexpdate" "+%Y-%m-%d %H:%M:%S")
    openvpnResults+="OpenVPN Keychain: $certCN | $certserial | Expiry Date: $certexpdateformatted\n"
    done
#Added this to differentiate between non-responsive systems and systems that just don't have a matching cert at all. 
#Esp useful in case you are running the contents from ARD (it works)
else
    openvpnResults="No Matching Certs Found in OpenVPN Keychain\n"
fi

#Consolidate results

RESULT="$systemResults$loginResults$openvpnResults"
/usr/bin/printf "<result>$RESULT</result>"

 

 

Claudius
New Contributor II

Thanks @Asri-Zainal,
that is certainly very helpful. But I am unfortunately not someone who can write scripts or knows anything about it.

The part about reading the AD certificates is about the most complicated thing we have in our Jamf environment.
I would be very, very grateful if someone could still add the part I need in our script (finding the AD certificate with the longest validity). I don't think I'll ever get anywhere here otherwise.

Thanks a lot for your help.

#!/bin/bash
COMPUTER_NAME=$(scutil --get ComputerName)
EXPIRATION_DATE=$(/usr/bin/security find-certificate -a -c "$COMPUTER_NAME" -p -Z "/Library/Keychains/System.keychain" | /usr/bin/openssl x509 -noout -enddate| cut -f2 -d=)
EXPIRATION_DATE_FORMATTED=$(/bin/date -j -f "%b %d %T %Y %Z" "$EXPIRATION_DATE" "+%d.%m.%Y %H:%M:%S")
echo "<result>$EXPIRATION_DATE_FORMATTED</result>"

 

dstranathan
Valued Contributor II

This should do the trick, as long as you provide the criteria for your machine certificate name. I used this in the past on our 802.1x EAP-TLS machine certificate for 802.1x in the System Keychain, but it can be used for any certs.

If only 1 cert is located and it is valid then it tells you (and reports the exp date for QA). If there are multiple certs with the same name then the oldest cert(s) are deleted.

I have not used it in a while, but I just tested it on Ventura and it worked.

(sorry for the weird formatting and possible line wrap. Cut and paste it into a text doc and tweak it there)

 

 

#!/bin/sh

# Script name: verify_and_delete_expired_computer_certs.sh
# Version: 1.0
# Author: DDS
# Creation date: August 10 2021
# Modification date: March 31 2022

COMPUTER_NAME=$( scutil --get ComputerName )
COMPUTER_CERTIFICATE_NAME="MY_CERT"
SYSTEM_KEYCHAIN="/Library/Keychains/System.keychain"
CURRENT_DATE_FORMATTED=$( date +%Y-%m-%d )                                                # Example: 2021-05-06 (no seconds!)
CURRENT_DATE_FORMATTED_TRUNCATED=$( echo ${CURRENT_DATE_FORMATTED} | tr -d "-" )          # Example: 20210506  (removed dashes)

function VERIFY_EXPIRED_CERTIFICATES() {

# Look through the entire System Keychain for any computer certificates that match the AD Name of the computer. 
# Parse the entire certificate and SHA Hash for ALL matches into an array, which is important if you are trying to locate duplicate certificates. 
# The IFS below "internal field separator" is perform a find-and-replace for separating 
# the search results with a , (1 comma) for each cert found that matched criteria.
# The sed find and replace operation  after "IFS" adds a , ( 1 comma) to the security tool's
# find to sort and display any potential duplicates (if there are any).
# Word-splitting in zsh is totally different than bash and other shells! IFS may not work as expected.

#IFS=","
local IFS=","


CERT_LIST=$( security find-certificate -a -c "${COMPUTER_CERTIFICATE_NAME}" -p -Z "${SYSTEM_KEYCHAIN}" | sed s/"-----END CERTIFICATE-----"/"-----END CERTIFICATE-----,"/g)

if [[ -n "${CERT_LIST}" ]]; then
    
    CERT_ARRAY=(${CERT_LIST})
    
    for CERT_ELEMENT in "${CERT_ARRAY[@]}"; do
    
        # Pass each matching certificate to openSSL, and pull expiration details from each matched certificate.
        # Each instance of the cert name will have a (number) appended.
        
        CERT_ARRAY_INDEX_NUMBER=$((${CERT_ARRAY_INDEX_NUMBER}+1))
        CERT_EXPIRATION_DATE_FULL=$( openssl x509 -noout -enddate <<< "${CERT_ELEMENT}" | cut -f2 -d= )
        CERT_EXPIRATION_DATE_FORMATTED=$( date -j -f "%b %d %T %Y %Z" "${CERT_EXPIRATION_DATE_FULL}" "+%Y-%m-%d" )
        CERT_EXPIRATION_DATE_FORMATTED_TRUNCATED=$( echo ${CERT_EXPIRATION_DATE_FORMATTED} | tr -d "-" )        
        
        #CERT_SERIAL=$( openssl x509 -noout -serial <<< $CERT_ELEMENT )
        CERT_SHA1_HASH=$( grep -e 'SHA-1 hash:' <<< "${CERT_ELEMENT}" | cut -d" "  -f3- )

        # More QA output for testing.
        #echo "SHA-1 hash: ${CERT_SHA1_HASH}"
        #echo "Certificate expiration date (full): ${CERT_EXPIRATION_DATE_FULL}"
        #echo "Certificate expiration date (formatted): ${CERT_EXPIRATION_DATE_FORMATTED}"
        #echo "Certificate expiration date (formatted and truncated): ${CERT_EXPIRATION_DATE_FORMATTED_TRUNCATED}"
        #echo
        #echo "Array elements: \n"
        #printf '%s\n' "${CERT_ARRAY[@]}"

        if [[ "${CERT_EXPIRATION_DATE_FORMATTED_TRUNCATED}" -eq "${CURRENT_DATE_FORMATTED_TRUNCATED}" ]]; then
    
            echo
            echo "Certificate containing string '${COMPUTER_CERTIFICATE_NAME}'(${CERT_ARRAY_INDEX_NUMBER}) (${CERT_SHA1_HASH}) EXPIRES TODAY (${CURRENT_DATE_FORMATTED}). No action is required."
            # Do not delete certificate.
            
        elif [[ "${CERT_EXPIRATION_DATE_FORMATTED_TRUNCATED}" -gt "${CURRENT_DATE_FORMATTED_TRUNCATED}" ]]; then
        
            echo
            echo "Certificate containing string '${COMPUTER_CERTIFICATE_NAME}' (${CERT_ARRAY_INDEX_NUMBER}) (${CERT_SHA1_HASH}) is VALID (Expiration: ${CERT_EXPIRATION_DATE_FORMATTED}). No action is required."
            # Do not delete certificate.
            
        elif [[ "${CERT_EXPIRATION_DATE_FORMATTED_TRUNCATED}" -lt "${CURRENT_DATE_FORMATTED_TRUNCATED}" ]]; then
    
            echo
            echo "Certificate containing string '${COMPUTER_CERTIFICATE_NAME}'(${CERT_ARRAY_INDEX_NUMBER}) (${CERT_SHA1_HASH}) has EXPIRED (${CERT_EXPIRATION_DATE_FORMATTED}). It will be deleted."
            # Delete certificate.
            DELETE_EXPIRED_CERIFICATES
            
        else
        
            echo
            echo "Alert: An error has occured. Certificate containing string '${COMPUTER_CERTIFICATE_NAME}' expiration is UNKNOWN."
            # Do not delete certificate.
            SCRIPT_POST_FLIGHT_1
    
        fi
    
    done
    
else
    
    echo
    echo "Alert: Certificate containing string '${COMPUTER_CERTIFICATE_NAME}' could not be found in ${SYSTEM_KEYCHAIN}."
    SCRIPT_POST_FLIGHT_1
    
fi

}

function DELETE_EXPIRED_CERIFICATES() {

    # Delete only the expired certificates
    
    echo "Deleting expired certificate containing string '${COMPUTER_CERTIFICATE_NAME}' (${CERT_SHA1_HASH}) from ${SYSTEM_KEYCHAIN}..."

    # The next line is dangerous!
    sudo security delete-certificate -Z ${CERT_SHA1_HASH} ${SYSTEM_KEYCHAIN}
    
    # Verify deletion status
    
    if [[ $? == 0 ]]; then
    
        echo
        echo "The expired certificate containing string ${COMPUTER_CERTIFICATE_NAME} was successfully removed from ${SYSTEM_KEYCHAIN}."
        
    else
        echo
        echo "Error: Failed to remove expired certificate containing string ${COMPUTER_CERTIFICATE_NAME}."
        exit 1
    
    fi
      
}

VERIFY_EXPIRED_CERTIFICATES

 

 




Thank you very much @dstranathan.
I ran the script on one of our test devices (just had to redefine the variables at the beginning) and it worked immediately.

On the test device with 2 valid certificates, the script told me at the end that no further steps were needed after the two valid certificates were displayed.
How would I have to extend the script, if I always want to delete the oldest certificate, even if it is still valid? Because that happens sometimes with us as well.
And if also still valid, but no longer necessary certificates are deleted, then everything still remains clear.

Many greetings
Claudius

Thank you very much @dstranathan.
I ran the script on one of our test devices (just had to redefine the variables at the beginning) and it worked immediately.

On the test device with 2 valid certificates, the script told me at the end that no further steps were needed after the two valid certificates were displayed.
How would I have to extend the script, if I always want to delete the oldest certificate, even if it is still valid? Because that happens sometimes with us as well.
And if also still valid, but no longer necessary certificates are deleted, then everything still remains clear.

Many greetings
Claudius