Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Check the signing certificates on pkg packages and apps
#!/bin/bash
#Joel "brunerd" Bruner - gets the certificate expiration(s) from a pkg or an app and outputs as CSV
#hold down command at launch or touch /tmp/debug to enable xtrace command expansion
commandKeyDown=$(/usr/bin/python -c 'import Cocoa; print Cocoa.NSEvent.modifierFlags() & Cocoa.NSCommandKeyMask > 1')
[ "$commandKeyDown" == "True" -o -f /tmp/debug ] && set -x && xtraceFlag=1
#############
# VARIABLES #
#############
#the name of this script
myName=$(basename "${0}")
myFolder="$(pwd)"
#a header for our CSV data
CSVHeader="Name,Expiration,Evaluation"
#for matching app evaluation
unsigned_codesign="code object is not signed at all"
#for matching pkg evaluation
expired_pkgutil="Status: signed by a certificate that has since expired"
valid_pkgutil="Status: signed by a certificate trusted by Mac OS X"
untrusted_pkgutil="Status: signed by untrusted certificate"
signed_pkgutil="Status: signed Apple Software"
unsigned_pkgutil="Status: no signature"
#for cert reconstruction later
head="-----BEGIN CERTIFICATE-----"
tail="-----END CERTIFICATE-----"
#############
# FUNCTIONS #
#############
function printUsage
{
echo -e "Usage: ${myName} [-a] [target] [...]\n"
echo -e "Switches:\n-a prints all cert expiration dates, otherwise only the cert expiring soonest is shown.\n"
echo -e "Arguments:\ntarget - can be one or more files or folders. All *pkg and *app items are examined within a folder.\n"
}
function checkPackageExpiration
{
local item="${1}"
local itemBasename="$(basename ${item})"
local output expirationX509 expirationEpoch expirationEpochPrevious
#see if the item is even signed
local checkSigOutput=$(pkgutil --check-signature "$item")
#get the signing status
local sigStatusString=$(grep 'Status: ' <<< "${checkSigOutput}" | sed 's/^[ \t]*//g')
#match it up and give it a friendly output name
case ${sigStatusString} in
"${expired_pkgutil}")
local sigStatusString_friendly="EXPIRED"
;;
"${valid_pkgutil}")
local sigStatusString_friendly="VALID"
;;
"${untrusted_pkgutil}")
local sigStatusString_friendly="UNTRUSTED"
;;
"${signed_pkgutil}")
local sigStatusString_friendly="SIGNED"
;;
"${unsigned_pkgutil}")
local sigStatusString_friendly="UNSIGNED"
;;
esac
#if something goes wrong
if [ "${sigStatusString_friendly}" = "UNSIGNED" ]; then
echo "${itemBasename},,UNSIGNED"
continue
fi
#create tab separated certs from the package xml (replace node with tab) and strip off all the xpath stuff and blank lines
local certificatesRAW=$(xpath //X509Certificate/'text()' 2>&1 <<< "$(xar --dump-toc=/dev/stdout -f "$item")" | sed -e 's/-- NODE --/'$'\t''/g' -e '/Found/d' -e '/^$/d' -e '/No nodes found/d')
#make an array of the certs
IFS=$'\t' GLOBIGNORE='*' command eval 'certArray=( ${certificatesRAW} )'
#loop through cert array and output expiration stats
for ((i=0; i < ${#certArray[@]}; i++)); do
#put header and footer on cert data
local fullCert="${head}"$'\n'"$(sed '/^$/d' <<< "${certArray[i]}")"$'\n'"${tail}"
#get expiration, get just the date, eliminate double spaces single digit days
expirationX509=$(openssl x509 -enddate -noout -in /dev/stdin <<< "${fullCert}" 2>/dev/null | awk -F'=' '{print $2}' | tr -s ' ')
#if something goes wrong
if [ -z "${expirationX509}" ]; then
echo "${itemBasename},,ERROR"
continue
fi
expirationEpoch=$(/bin/date -j -f "%b %d %T %Y %Z" "${expirationX509}" "+%s")
expirationSortable=$(/bin/date -j -f "%b %d %T %Y %Z" "${expirationX509}" "+%Y-%m-%d %H:%M:%S %Z")
#build output string(s) for all certs
if [ -n "${allCerts_flag}" ]; then
#build the multi-line output
[ -z "${output}" ] && output+="${itemBasename},${expirationSortable},${sigStatusString_friendly}" || output+=$'\n'"${itemBasename},${expirationSortable},${sigStatusString_friendly}"
#only have the lowest number
else
#if we haven't set that
if [ -z "${expirationEpochPrevious}" ]; then
#set the output string
output="$(basename "${item}"),${expirationSortable},${sigStatusString_friendly}"
#if we are lower than the last one add that to the output
elif [ "${expirationEpoch}" -lt "${expirationEpochPrevious}" ]; then
#set the output string
output="$(basename "${item}"),${expirationSortable},${sigStatusString_friendly}"
fi
fi
#set this for the next loop
expirationEpochPrevious="${expirationEpoch}"
done
if [ -n "${allCerts_flag}" ]; then
#output sorted uniq string - when we were doing multiline output
output=$(sort -r <<< "${output}" | uniq)
fi
#only if we have output echo it
[ -n "${output}" ] && echo "${output}"
}
function checkAppExpiration
{
local item="${1}"
#if we have a relative path make it full so the cert extrations routines work
[ "${item:0:1}" = "." ] && local item="$(pwd)/${item}"
local itemBasename="$(basename ${item})"
local file output expirationX509 expirationEpoch expirationEpochPrevious checkSigOutput
#check the signing status of the app, capture output (why do they make the output go to stderr?)
#note: if local precedes this variable the exitCode will always be 0
checkSigOutput=$(codesign -d -vvv "${item}" 2>&1)
local exitCode=$?
#non zero means it could not find a cert, report and continue
if [ "${exitCode}" -gt 0 ]; then
#get the signing status - expecting single line output
local sigStatusString=$(awk -F": " '{print $2}' <<< "${checkSigOutput}")
#match the output and make it friendly
case ${sigStatusString} in
"${unsigned_codesign}")
local sigStatusString_friendly="UNSIGNED"
;;
*)
local sigStatusString_friendly="ERROR"
;;
esac
#output and go to the next
echo "${itemBasename},,${sigStatusString_friendly}"
continue
fi
#make a place for the exported certs
local randomFolder="/tmp/certfiles-$RANDOM"
mkdir "${randomFolder}"
#the tool does not let us specify an output folder, we just have to go there
cd "${randomFolder}"
#export the certs quietly to the current directory (no other way)
codesign -dvvvv --extract-certificates "${item}" 2>/dev/null
#go back where we were (or else there's problems)
cd "${myFolder}"
#get all the codesign files
local fileList=$(find "${randomFolder}" -name "codesign*")
IFS=$'\n' #not expecting spaces, just habit
#loop through cert array and output expiration stats
for file in ${fileList}; do
#get expiration, get just the date, eliminate double spaces single digit days
expirationX509=$(openssl x509 -enddate -noout -inform DER -in "${file}" | awk -F'=' '{print $2}' | tr -s ' ')
#if something goes wrong
if [ -z "${expirationX509}" ]; then
echo "${itemBasename},,ERROR"
continue
fi
#get the date - 3 different ways
expirationEpoch=$(/bin/date -j -f "%b %d %T %Y %Z" "${expirationX509}" "+%s")
expirationSortable=$(/bin/date -j -f "%b %d %T %Y %Z" "${expirationX509}" "+%Y-%m-%d %H:%M:%S %Z")
epochTimeNow=$(date +"%s")
#simple comparison no other validation done
if [ "${expirationEpoch}" -lt "${epochTimeNow}" ]; then
sigStatusString_friendly="EXPIRED"
else
sigStatusString_friendly="VALID"
fi
#if -a flag, build output string(s) for all certs
if [ -n "${allCerts_flag}" ]; then
#build the multi-line output
[ -z "${output}" ] && output+="${itemBasename},${expirationSortable},${sigStatusString_friendly}" || output+=$'\n'"${itemBasename},${expirationSortable},${sigStatusString_friendly}"
#or output the nearest expiration
else
#if we haven't set that
if [ -z "${expirationEpochPrevious}" ]; then
#set the output string
output="${itemBasename},${expirationSortable},${sigStatusString_friendly}"
#if we are lower than the last one add that to the output
elif [ "${expirationEpoch}" -lt "${expirationEpochPrevious}" ]; then
#set the output string
output="${itemBasename},${expirationSortable},${sigStatusString_friendly}"
fi
fi
#set this for the next loop
expirationEpochPrevious="${expirationEpoch}"
done
#cleanup
rm -rf "${randomFolder}"
if [ -n "${allCerts_flag}" ]; then
#output sorted uniq string - when we were doing multiline output
output=$(sort -r <<< "${output}" | uniq)
fi
#only if we have output echo it
[ -n "${output}" ] && echo "${output}"
}
function processTarget
{
#strip any trailing slash off
local target="${1%/}"
local item;
IFS=$'\n'
#if directory get all the .(m)pkg or .app files/folders inside
if [ -d "${target}" ]; then
fileList=$(find "${target}" -name '*pkg' -or -name '*app' | sort)
else
fileList="${target}"
fi
#got through each file
for item in ${fileList}; do
#trim every thing before the last dor
itemExtension=${item##*.}
if [ "${itemExtension}" = "pkg" ]; then
checkPackageExpiration "${item}"
elif [ "${itemExtension}" = "app" ]; then
checkAppExpiration "${item}"
fi
done
}
########
# MAIN #
########
#options processing
while getopts ":ah" option; do
case "${option}" in
#print all certs
'a')
#flag vars simply need to be set or not
allCerts_flag='Y'
;;
'h')
printUsage
exit
;;
esac
done
#shift past options so $1 is still $1 after the options are processed
shift $((OPTIND-1))
#if no arguments after options processing, offer help
#btw you cannot yest ${@} becuase multiple items appear as multiple arguments to [ (test) so we eval an echo of it
if [ -z "$(eval echo ${@})" ]; then
#let you know there an option or two to be had
echo "Please provide target(s) to examine..."
echo "Run \"${myName} -h\" for help"
exit
fi
#only respect newlines in the for loop
IFS=$'\n'
echo "${CSVHeader}"
#keep going until you can't do no more
for target in ${@}; do
processTarget "${target}"
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.