Last active
April 12, 2024 07:25
-
-
Save trscavo/056a519964f136ce77df to your computer and use it in GitHub Desktop.
Bash script to inspect a SAML metadata file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
########################################################### | |
# Inspect a SAML metadata file and report its characteristics. | |
# | |
# Usage: md_inspect.sh [-vs] [MD_FILE] | |
# | |
# Optionally takes the path to the metadata file as a command-line | |
# parameter. If none is given, takes its input from stdin instead. | |
# | |
# The -s option enables an advanced "Security Reporting Mode." | |
# When enabled, the script reports advanced security-related aspects | |
# of the metadata file, including the fingerprint of the signing | |
# certificate, the key size, the SignatureMethod, the DigestMethod, | |
# and so forth. | |
# | |
# Examples: | |
# | |
# $ curl --remote-name http://md.example.org/some-metadata.xml | |
# $ md_inspect.sh some-metadata.xml | |
# Metadata file name: some-metadata.xml | |
# Metadata size: 11032748 bytes | |
# ... | |
# | |
# $ cat some-metadata.xml | md_inspect.sh -v | |
# md_inspect.sh using temp dir: /tmp | |
# Metadata file name: (stdin) | |
# Metadata file size: 11032748 bytes | |
# ... | |
# | |
########################################################### | |
script_name=${0##*/} # equivalent to basename $0 | |
# process command-line option(s) | |
verbose_mode=false; security_reporting_mode=false | |
while getopts ":vs" opt; do | |
case $opt in | |
v) | |
verbose_mode=true | |
;; | |
s) | |
security_reporting_mode=true | |
;; | |
\?) | |
echo "ERROR: $script_name: Unrecognized option: -$OPTARG" >&2 | |
exit 2 | |
;; | |
esac | |
done | |
# make sure there is at most one command-line argument | |
shift $(( OPTIND - 1 )) | |
if [ $# -gt 1 ]; then | |
echo "ERROR: $script_name: too many arguments: $# (0 or 1 required)" >&2 | |
exit 2 | |
fi | |
# create a temporary directory | |
tmp_dir=$( mktemp -d 2>/dev/null || mktemp -d -t "${script_name%%.*}" ) | |
if [ ! -d "$tmp_dir" ] ; then | |
printf "ERROR: Unable to create temporary dir\n" >&2 | |
exit 2 | |
fi | |
$verbose_mode && printf "$script_name using temp dir: %s\n" "$tmp_dir" | |
# read the input into a temporary file | |
md_file=${tmp_dir}/tmp_metadata.xml | |
if [ $# -eq 1 ]; then | |
if [ ! -f "$1" ] ; then | |
printf "ERROR: The metadata file does not exist: %s\n" "$1" >&2 | |
exit 2 | |
fi | |
file_name="$1" | |
# copy input file into the temp file | |
cat "$1" > "$md_file" | |
else | |
file_name='(stdin)' | |
# read input from stdin into the temp file | |
cat - > "$md_file" | |
fi | |
# Does the file contain an aggregate of SAML metadata? | |
entities_descriptors=$( cat "$md_file" | grep -E '<(md:)?EntitiesDescriptor ' ) | |
if [ -z "$entities_descriptors" ]; then | |
printf "ERROR: The file is NOT a SAML metadata aggregate: %s\n" "$md_file" >&2 | |
exit 2 | |
fi | |
num_descriptors=$( echo "$entities_descriptors" | wc -l ) | |
if [ "$num_descriptors" -gt 1 ]; then | |
printf "ERROR: Multiple EntitiesDescriptor elements found: %d\n" "$num_descriptors" >&2 | |
exit 2 | |
fi | |
report_security_features () { | |
local signature | |
local certificate | |
local cert | |
local encoded_cert | |
local parsed_cert | |
local exit_status | |
local keySize | |
local fingerprint | |
local signatureMethod | |
local sigAlgorithm | |
local digestMethod | |
local digestAlgorithm | |
# make sure there is exactly one command-line argument | |
if [ $# -ne 1 ]; then | |
echo "ERROR: $FUNCNAME: incorrect number of arguments: $# (1 required)" >&2 | |
return 2 | |
fi | |
# extract the <ds:Signature> element | |
signature=$( /bin/cat "$1" \ | |
| sed -n -e '\;<\(ds:\)\{0,1\}Signature;,\;Signature>;p' | |
) | |
# extract the <ds:X509Certificate> element | |
certificate=$( echo "$signature" \ | |
| sed -n -e '\;<\(ds:\)\{0,1\}X509Certificate;,\;X509Certificate>;p' | |
) | |
# extract the cert content | |
cert=$( echo "$certificate" \ | |
| sed -e 's/^.*<\(ds:\)\{0,1\}X509Certificate>//' \ | |
| sed -e 's/<\/\(ds:\)\{0,1\}X509Certificate>.*$//' \ | |
| grep '[^[:blank:]]' | |
) | |
# encode the cert content | |
encoded_cert=$( | |
printf "%s\n" '-----BEGIN CERTIFICATE-----' "$cert" '-----END CERTIFICATE-----' | |
) | |
# pipe the encoded cert into openssl | |
parsed_cert=$( echo "$encoded_cert" \ | |
| /usr/bin/openssl x509 -modulus -fingerprint -sha1 -noout ) | |
exit_status=$? | |
if [ $exit_status -ne 0 ]; then | |
echo "ERROR: $FUNCNAME: openssl failed (${exit_status}) on cert:" >&2 | |
echo "$encoded_cert" >&2 | |
return 1 | |
fi | |
# compute and print the key size | |
keySize=$(( | |
4 * $( echo "${parsed_cert}" | grep "^Modulus=" \ | |
| cut -d= -f2 | /usr/bin/tr -d "\r\n" | /usr/bin/wc -c ) | |
)) | |
printf "Metadata signing key size: %d bits\n" "$keySize" | |
# compute and print the fingerprint | |
fingerprint=$(echo "${parsed_cert}" | grep "Fingerprint=" | cut -d= -f2) | |
printf "Metadata signing certificate (SHA-1 fingerprint): %s\n" "$fingerprint" | |
# determine the SignatureMethod algorithm | |
signatureMethod=$( echo "$signature" | grep -E '<(ds:)?SignatureMethod ' ) | |
if [ -z "$signatureMethod" ]; then | |
sigAlgorithm='(unknown)' | |
else | |
sigAlgorithm=$( echo "$signatureMethod" \ | |
| sed -e 's/^.*SignatureMethod Algorithm="\([^"]*\)".*$/\1/' | |
) | |
fi | |
printf "Metadata signature algorithm (SignatureMethod): %s\n" "$sigAlgorithm" | |
# determine the DigestMethod algorithm | |
digestMethod=$( echo "$signature" | grep -E '<(ds:)?DigestMethod ' ) | |
if [ -z "$digestMethod" ]; then | |
digestAlgorithm='(unknown)' | |
else | |
digestAlgorithm=$( echo "$digestMethod" \ | |
| sed -e 's/^.*DigestMethod Algorithm="\([^"]*\)".*$/\1/' | |
) | |
fi | |
printf "Metadata digest algorithm (DigestMethod): %s\n" "$digestAlgorithm" | |
} | |
##################################################################### | |
# Begin report | |
##################################################################### | |
# What is the name of the metadata file? | |
printf "Metadata file name: %s\n" "$file_name" | |
# How large is the metadata file? | |
file_size=$( cat "$md_file" | wc -c ) | |
printf "Metadata file size: %d bytes\n" $file_size | |
# What is the name of this group of entities? | |
entities_descriptor=$( echo "$entities_descriptors" | head -n 1 ) | |
if echo "$entities_descriptor" | grep -Fq 'Name='; then | |
entities_name=$( echo "$entities_descriptor" \ | |
| sed -e 's/^.*Name="\([^"]*\)".*$/\1/' | |
) | |
else | |
entities_name='(none)' | |
fi | |
printf "Metadata entity group: %s\n" "$entities_name" | |
# Does the file contain a PublicationInfo extension element? | |
# If so, what is the publisher name and the publication date? | |
pub_info_elements=$( cat "$md_file" | grep -E '<(mdrpi:)?PublicationInfo ' ) | |
if [ -z "$pub_info_elements" ]; then | |
num_pub_info_elements=0 | |
publisher='(unknown)' | |
creationInstant='(unknown)' | |
else | |
num_pub_info_elements=$( echo "$pub_info_elements" | wc -l ) | |
pub_info_element=$( echo "$pub_info_elements" | head -n 1 ) | |
publisher=$( echo "$pub_info_element" \ | |
| sed -e 's/^.* publisher="\([^"]*\)".*$/\1/' | |
) | |
creationInstant=$( echo "$pub_info_element" \ | |
| sed -e 's/^.* creationInstant="\([^"]*\)".*$/\1/' | |
) | |
fi | |
printf "Metadata publisher: %s\n" "$publisher" | |
printf "Metadata publication date: %s\n" "$creationInstant" | |
if [ "$num_pub_info_elements" -gt 1 ]; then | |
printf "WARNING: Unexpected number of PublicationInfo elements: %d\n" $num_pub_info_elements | |
fi | |
# What is the expiration date on the metadata aggregate? | |
if echo "$entities_descriptor" | grep -Fq 'validUntil='; then | |
is_valid_until=true | |
valid_until=$( echo "$entities_descriptor" \ | |
| sed -e 's/^.*validUntil="\([^"]*\)".*$/\1/' | |
) | |
else | |
is_valid_until=false | |
valid_until='(none)' | |
fi | |
printf "Metadata expiration date (validUntil): %s\n" "$valid_until" | |
if ! $is_valid_until; then | |
printf "WARNING: There is NO validUntil XML attribute\n" | |
fi | |
# What is the suggested refresh interval on the metadata aggregate? | |
if echo "$entities_descriptor" | grep -Fq 'cacheDuration='; then | |
cache_duration=$( echo "$entities_descriptor" \ | |
| sed -e 's/^.*cacheDuration="\([^"]*\)".*$/\1/' | |
) | |
else | |
cache_duration='(none)' | |
fi | |
printf "Metadata refresh interval (cacheDuration): %s\n" "$cache_duration" | |
# Is the metadata signed? | |
if cat "$md_file" | grep -Eq '<(ds:)?Signature[ >]'; then | |
printf "Metadata file is signed" | |
# optionally report additional security-related features of the metadata file | |
if $security_reporting_mode; then | |
printf "\n" | |
output=$( report_security_features "$md_file" ) | |
return_status=$? | |
if [ $return_status -ne 0 ]; then | |
echo "ERROR: unable to determine security features of metadata file: ${return_status}" >&2 | |
else | |
echo "$output" | |
fi | |
else | |
printf " (use -s option for advanced security features)\n" | |
fi | |
else | |
printf "WARNING: Metadata file is NOT signed\n" | |
fi | |
# How many (unique) registrars in the aggregate? | |
reg_info_elements=$( cat "$md_file" | grep -E '<(mdrpi:)?RegistrationInfo ' ) | |
if [ -z "$reg_info_elements" ]; then | |
printf "Metadata registrar: (unknown)\n" | |
else | |
num_registrars=$( echo "$reg_info_elements" \ | |
| sed -e 's/^.* registrationAuthority="\([^"]*\)".*$/\1/' \ | |
| sort | uniq \ | |
| wc -l | |
) | |
if [ "$num_registrars" -eq 1 ]; then | |
registrar=$( echo "$reg_info_elements" \ | |
| grep -F -m 1 ' registrationAuthority=' \ | |
| sed -e 's/^.* registrationAuthority="\([^"]*\)".*$/\1/' | |
) | |
printf "Metadata registrar: %s\n" "$registrar" | |
else | |
printf "Number of registrars: %d\n" $num_registrars | |
fi | |
fi | |
# How many (unique) organizations own the entity descriptors? | |
num_orgs=$( cat "$md_file" \ | |
| grep -E '<(md:)?OrganizationName xml:lang="en">' \ | |
| sed -e 's/^.*OrganizationName xml:lang="en">\([^<]*\).*$/\1/' \ | |
| sort | uniq \ | |
| wc -l | |
) | |
printf "Number of organizations: %d\n" $num_orgs | |
# How many SAML entity descriptors in the aggregate? | |
num_entities=$( cat "$md_file" \ | |
| grep -E '<(md:)?EntityDescriptor ' \ | |
| wc -l | |
) | |
printf "Number of SAML entities: %d\n" $num_entities | |
# How many SAML entityIDs in the aggregate? (sanity check) | |
num_entityIDs=$( cat "$md_file" \ | |
| grep -F ' entityID=' \ | |
| wc -l | |
) | |
if [ "$num_entities" -ne "$num_entityIDs" ]; then | |
printf "WARNING: Unexpected number of entityIDs: %d\n" $num_entityIDs | |
fi | |
# How many IdP roles in the aggregate? | |
num_idp_roles=$( cat "$md_file" \ | |
| grep -E '<(md:)?IDPSSODescriptor ' \ | |
| wc -l | |
) | |
printf "Number of IdP roles: %d\n" $num_idp_roles | |
# How many IdP roles support SAML V2.0? | |
num_idp_roles_saml2=$( cat "$md_file" \ | |
| grep -E '<(md:)?IDPSSODescriptor ' \ | |
| grep ' protocolSupportEnumeration=".*SAML:2.0:protocol.*"' \ | |
| wc -l | |
) | |
printf "Number of IdP roles that support SAML2: %d\n" $num_idp_roles_saml2 | |
# How many IdP roles with an errorURL? | |
num_idp_roles_errorurl=$( cat "$md_file" \ | |
| grep -E '<(md:)?IDPSSODescriptor ' \ | |
| grep ' errorURL=".*"' \ | |
| wc -l | |
) | |
printf "Number of IdP roles with an errorURL: %d\n" $num_idp_roles_errorurl | |
# How many SP roles in the aggregate? | |
num_sp_roles=$( cat "$md_file" \ | |
| grep -E '<(md:)?SPSSODescriptor ' \ | |
| wc -l | |
) | |
printf "Number of SP roles: %d\n" $num_sp_roles | |
# How many SP roles support SAML V2.0? | |
num_sp_roles_saml2=$( cat "$md_file" \ | |
| grep -E '<(md:)?SPSSODescriptor ' \ | |
| grep ' protocolSupportEnumeration=".*SAML:2.0:protocol.*"' \ | |
| wc -l | |
) | |
printf "Number of SP roles that support SAML2: %d\n" $num_sp_roles_saml2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment