Skip to content

Instantly share code, notes, and snippets.

@trscavo
Last active April 12, 2024 07:25
Show Gist options
  • Save trscavo/056a519964f136ce77df to your computer and use it in GitHub Desktop.
Save trscavo/056a519964f136ce77df to your computer and use it in GitHub Desktop.
Bash script to inspect a SAML metadata file
#!/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