Skip to content

Instantly share code, notes, and snippets.

@timhodson
Last active December 12, 2025 10:08
Show Gist options
  • Select an option

  • Save timhodson/a4acee9965926c2338258448d7fbe558 to your computer and use it in GitHub Desktop.

Select an option

Save timhodson/a4acee9965926c2338258448d7fbe558 to your computer and use it in GitHub Desktop.
Find and print information about each of the certificates found in a SAML federation metadata file hosted online
#!/bin/bash
# Script to check all certificates in SAML metadata
# Prints expiry, use attribute, and fingerprint for each certificate found
read -d '' HELP <<- PLEH
Usage: ./check-saml-certs <metadataURL|localFilePath|certificateFile>
Fetches SAML metadata from the given URL or local file and reports:
- Entity ID
- Total certificates found
- For each certificate:
* Location (XPath)
* Use attribute (signing/encryption/unknown)
* SHA-512 Fingerprint
* Expiry date
* Days remaining until expiry
If a certificate file (.crt, .pem, .cer, .der) is provided:
- Displays certificate subject, issuer, validity dates
- Calculates SHA-512 fingerprint
- Shows days remaining until expiry
Examples:
./check-saml-certs https://example.edu/idp/metadata
./check-saml-certs /path/to/metadata.xml
./check-saml-certs ./certificate.crt
./check-saml-certs ./certificate.pem
PLEH
METADATA_URL=$1
if [[ -z ${METADATA_URL} ]]; then
echo "Error: Metadata URL is required"
echo "$HELP"
exit 1
fi
# Check if it's a certificate file
if [[ -f "${METADATA_URL}" ]] && [[ "${METADATA_URL}" =~ \.(crt|pem|cer|der|key)$ ]]; then
echo -e "==== Certificate File Analysis ===="
echo -e "- File:\t\t${METADATA_URL}\n"
# Test if certificate can be read (try PEM first, then DER)
openssl x509 -in "${METADATA_URL}" -noout 2>/dev/null
if [[ $? -ne 0 ]]; then
# Try DER format
openssl x509 -in "${METADATA_URL}" -inform DER -noout 2>/dev/null
if [[ $? -ne 0 ]]; then
echo "Error: Unable to read certificate file"
exit 1
fi
CERT_FORMAT="-inform DER"
else
CERT_FORMAT=""
fi
# Extract key information
SUBJECT=$(openssl x509 -in "${METADATA_URL}" ${CERT_FORMAT} -noout -subject 2>/dev/null | sed 's/subject=//')
ISSUER=$(openssl x509 -in "${METADATA_URL}" ${CERT_FORMAT} -noout -issuer 2>/dev/null | sed 's/issuer=//')
STARTDATE=$(openssl x509 -in "${METADATA_URL}" ${CERT_FORMAT} -noout -startdate 2>/dev/null | sed 's/notBefore=//')
ENDDATE=$(openssl x509 -in "${METADATA_URL}" ${CERT_FORMAT} -noout -enddate 2>/dev/null | sed 's/notAfter=//')
# Calculate SHA-512 fingerprint using gsha512sum (matching metadata cert method)
CERT_BASE64=$(openssl x509 -in "${METADATA_URL}" ${CERT_FORMAT} 2>/dev/null | grep -v "BEGIN CERTIFICATE" | grep -v "END CERTIFICATE" | tr -d '\n' | tr -d ' ')
FINGERPRINT=$(echo -n "${CERT_BASE64}" | gsha512sum | tr "[:lower:]" "[:upper:]" | cut -f 1 -d " ")
# Calculate days remaining
REMAIN_DAYS=""
if [[ -n "${ENDDATE}" ]]; then
END_EPOCH=$(date -j -f "%b %e %H:%M:%S %Y %Z" "${ENDDATE}" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
if [[ -n "${END_EPOCH}" ]]; then
REMAIN_DAYS=$(( (END_EPOCH - NOW_EPOCH) / 86400 ))
fi
fi
echo -e "Subject:\t${SUBJECT}"
echo -e "Issuer:\t\t${ISSUER}"
echo -e "Valid From:\t${STARTDATE}"
echo -e "Valid Until:\t${ENDDATE}"
if [[ -n "${REMAIN_DAYS}" ]]; then
if [[ ${REMAIN_DAYS} -lt 0 ]]; then
echo -e "Status:\t\tEXPIRED (${REMAIN_DAYS#-} days ago)"
elif [[ ${REMAIN_DAYS} -lt 30 ]]; then
echo -e "Days Left:\t${REMAIN_DAYS} ⚠️ EXPIRING SOON"
else
echo -e "Days Left:\t${REMAIN_DAYS}"
fi
fi
echo -e "SHA-512:\t${FINGERPRINT}"
echo ""
echo "==== Analysis Complete ===="
exit 0
fi
# Convert local file path to file:// URL if needed
if [[ ! "${METADATA_URL}" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*:// ]]; then
# Not a URL scheme, treat as local file path
if [[ -f "${METADATA_URL}" ]]; then
# Convert to absolute path if relative
if [[ "${METADATA_URL}" != /* ]]; then
METADATA_URL="$(cd "$(dirname "${METADATA_URL}")" && pwd)/$(basename "${METADATA_URL}")"
fi
METADATA_URL="file://${METADATA_URL}"
elif [[ ! -e "${METADATA_URL}" ]]; then
echo "Error: Local file not found: $1"
exit 1
fi
fi
echo -e "==== SAML Metadata Certificate Check ===="
echo -e "- Metadata URL\t: ${METADATA_URL}\n"
# Fetch metadata
METADATA_XML=$(curl -s -A "Talis Metadata Check" ${METADATA_URL})
if [[ -z "${METADATA_XML}" ]]; then
echo "Error: Failed to fetch metadata from ${METADATA_URL}"
exit 1
fi
# Extract entity ID
ENTITY_ID=$(xpath -e '//*[local-name()="EntityDescriptor"]/@entityID' <(echo ${METADATA_XML}) 2> /dev/null | cut -d '=' -f 2)
# Find all X509Certificate elements (compatible with older bash)
CERTS=()
while IFS= read -r line; do
[[ -n "$line" ]] && CERTS+=("$line")
done < <(xpath -e '//*[local-name()="X509Certificate"]/text()' <(echo ${METADATA_XML}) 2> /dev/null | sed -E 's/[[:space:]]+//g' | sed '/^$/d')
if [[ ${#CERTS[@]} -eq 0 ]]; then
echo "No X509Certificate elements found in metadata"
exit 1
fi
echo -e "- Entity ID\t: ${ENTITY_ID}"
echo -e "- Certificates\t: ${#CERTS[@]} found\n"
# Extract KeyDescriptor use attributes (best effort alignment with cert order)
USES=()
while IFS= read -r line; do
[[ -n "$line" ]] && USES+=("$line")
done < <(xpath -e '//*[local-name()="X509Certificate"]/ancestor::*[local-name()="KeyDescriptor"]/attribute::use' <(echo ${METADATA_XML}) 2> /dev/null | sed -E 's/^[^=]*=//; s/"//g')
# Build a simple index to track certificate locations
# We'll identify whether each cert is in a KeyDescriptor or Signature element
PATHS=()
# Check KeyDescriptor certificates
while IFS= read -r use_attr; do
if [[ -n "$use_attr" ]]; then
PATHS+=("KeyDescriptor[@use='${use_attr}']/KeyInfo/X509Data/X509Certificate")
else
PATHS+=("KeyDescriptor/KeyInfo/X509Data/X509Certificate")
fi
done < <(xpath -e '//*[local-name()="KeyDescriptor"]//*[local-name()="X509Certificate"]/ancestor::*[local-name()="KeyDescriptor"]/@use' <(echo ${METADATA_XML}) 2> /dev/null | sed -E 's/^[^=]*=//; s/"//g')
# Check Signature certificates (these won't have KeyDescriptor)
sig_count=$(xpath -e 'count(//*[local-name()="Signature"]//*[local-name()="X509Certificate"])' <(echo ${METADATA_XML}) 2> /dev/null)
if [[ -n "$sig_count" ]] && [[ "$sig_count" != "0" ]]; then
for ((i=0; i<sig_count; i++)); do
PATHS+=("Signature/KeyInfo/X509Data/X509Certificate")
done
fi
# If we still don't have enough paths, fill with generic ones
while [[ ${#PATHS[@]} -lt ${#CERTS[@]} ]]; do
PATHS+=("X509Certificate[position()=$((${#PATHS[@]}+1))]")
done
# Process each certificate
idx=0
for CERT in "${CERTS[@]}"; do
# Calculate SHA-512 fingerprint
FINGERPRINT=$(echo -n $CERT | gsha512sum | tr "[:lower:]" "[:upper:]" | cut -f 1 -d " ")
# Build PEM format for openssl
PEM_CERT=$(echo -e "-----BEGIN CERTIFICATE-----\n${CERT}\n-----END CERTIFICATE-----")
# Extract expiry date
ENDDATE_LINE=$(openssl x509 -enddate -noout 2>/dev/null <<< "${PEM_CERT}")
ENDDATE=${ENDDATE_LINE#notAfter=}
# Calculate days remaining
REMAIN_DAYS=""
if [[ -n "${ENDDATE}" ]]; then
END_EPOCH=$(date -j -f "%b %e %H:%M:%S %Y %Z" "${ENDDATE}" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
if [[ -n "${END_EPOCH}" ]]; then
REMAIN_DAYS=$(( (END_EPOCH - NOW_EPOCH) / 86400 ))
fi
fi
# Get use attribute (signing/encryption/unknown)
USE_LABEL="${USES[$idx]}"
[[ -z "${USE_LABEL}" ]] && USE_LABEL="unknown"
# Get path location
PATH_LABEL="${PATHS[$idx]}"
[[ -z "${PATH_LABEL}" ]] && PATH_LABEL="X509Certificate[$((idx+1))]"
# Print certificate details
echo -e "==================== Certificate #$((idx+1)) ===================="
echo -e "Location:\t${PATH_LABEL}"
echo -e "Use:\t\t${USE_LABEL}"
echo -e "Fingerprint:\t${FINGERPRINT}"
if [[ -n "${ENDDATE}" ]]; then
echo -e "Expiry:\t\t${ENDDATE}"
if [[ -n "${REMAIN_DAYS}" ]]; then
if [[ ${REMAIN_DAYS} -lt 0 ]]; then
echo -e "Status:\t\tEXPIRED (${REMAIN_DAYS#-} days ago)"
elif [[ ${REMAIN_DAYS} -lt 30 ]]; then
echo -e "Days Left:\t${REMAIN_DAYS} ⚠️ EXPIRING SOON"
else
echo -e "Days Left:\t${REMAIN_DAYS}"
fi
fi
else
echo -e "Expiry:\t\t(unavailable)"
fi
echo ""
idx=$((idx+1))
done
echo "==== Check Complete ===="
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment