From 3b1394639f49c0c5fd7c457dda2eef2807024df7 Mon Sep 17 00:00:00 2001 From: Tom Scavo Date: Sat, 10 Dec 2016 16:51:22 -0500 Subject: [PATCH] Initial commit of probe_saml_idp.sh and saml_tools.sh --- bin/probe_saml_idp.sh | 334 +++++++++++++++++ install.sh | 4 +- lib/saml_tools.sh | 811 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1148 insertions(+), 1 deletion(-) create mode 100755 bin/probe_saml_idp.sh create mode 100755 lib/saml_tools.sh diff --git a/bin/probe_saml_idp.sh b/bin/probe_saml_idp.sh new file mode 100755 index 0000000..00f8408 --- /dev/null +++ b/bin/probe_saml_idp.sh @@ -0,0 +1,334 @@ +#!/bin/bash + +####################################################################### +# Copyright 2016 Internet2 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################### + +####################################################################### +# Bootstrap +####################################################################### + +script_bin=${0%/*} # equivalent to dirname $0 +script_name=${0##*/} # equivalent to basename $0 + +# determine the source lib directory +if [ -z "$LIB_DIR" ]; then + echo "ERROR: $script_name requires env var LIB_DIR" >&2 + exit 2 +fi +if [ ! -d "$LIB_DIR" ]; then + echo "ERROR: $script_name: LIB_DIR does not exist: $LIB_DIR" >&2 + exit 2 +fi + +# library filenames (always list command_paths first) +LIB_FILENAMES="command_paths.sh +compatible_mktemp.sh +http_tools.sh +md_tools.sh +saml_tools.sh" + +# source lib files +for lib_filename in $LIB_FILENAMES; do + lib_file="$LIB_DIR/$lib_filename" + if [ ! -f "$lib_file" ]; then + echo "ERROR: $script_name: lib file does not exist: $lib_file" >&2 + exit 2 + fi + source "$lib_file" + status_code=$? + if [ $status_code -ne 0 ]; then + echo "ERROR: $script_name failed to source lib file ($status_code) $lib_file" >&2 + exit 2 + fi +done + +# default parameters +connect_timeout_default=2 +max_redirs_default=7 + +# default env vars +saml2_sp_entity_id_default=https://fm.incommon.org/sp +saml2_sp_acs_url_default=https://service1.internet2.edu/Shibboleth.sso/SAML2/POST +saml2_sp_acs_binding_default=urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST + +# check env vars, set defaults if neccessary +if [ -z "$SAML2_SP_ENTITY_ID" ]; then + echo "WARNING: $script_name: env var SAML2_SP_ENTITY_ID does not exist, using default: $saml2_sp_entity_id_default" >&2 + SAML2_SP_ENTITY_ID=$saml2_sp_entity_id_default +fi +if [ -z "$SAML2_SP_ACS_URL" ]; then + echo "WARNING: $script_name: env var SAML2_SP_ACS_URL does not exist, using default: $saml2_sp_acs_url_default" >&2 + SAML2_SP_ACS_URL=$saml2_sp_acs_url_default +fi +if [ -z "$SAML2_SP_ACS_BINDING" ]; then + echo "WARNING: $script_name: env var SAML2_SP_ACS_BINDING does not exist, using default: $saml2_sp_acs_binding_default" >&2 + SAML2_SP_ACS_BINDING=$saml2_sp_acs_binding_default +fi + +####################################################################### +# Process command-line options and arguments +####################################################################### + +help_mode=false; quiet_mode=false; verbose_mode=false +md_query_mode=false; md_file_mode=false +local_opts=; connect_timeout=; max_time=; max_redirs= +while getopts ":hqvt:m:r:u:f:" opt; do + case $opt in + h) + help_mode=true + ;; + q) + quiet_mode=true + verbose_mode=false + #local_opts="$local_opts -$opt" + ;; + v) + quiet_mode=false + verbose_mode=true + local_opts="$local_opts -$opt" + ;; + t) + connect_timeout="$OPTARG" + local_opts="$local_opts -t $OPTARG" + ;; + m) + max_time="$OPTARG" + local_opts="$local_opts -m $OPTARG" + ;; + r) + max_redirs="$OPTARG" + local_opts="$local_opts -r $OPTARG" + ;; + u) + md_query_mode=true + md_file_mode=false + mdq_base_url="$OPTARG" + ;; + f) + md_query_mode=false + md_file_mode=true + md_path="$OPTARG" + ;; + \?) + echo "ERROR: $script_name: Unrecognized option: -$OPTARG" >&2 + exit 2 + ;; + :) + echo "ERROR: $script_name: Option -$OPTARG requires an argument" >&2 + exit 2 + ;; + esac +done + +if $help_mode; then + display_help + exit 0 +fi + +# report bootstrap operations +if $verbose_mode; then + printf "$script_name using source lib directory: %s\n" "$LIB_DIR" + for lib_filename in $LIB_FILENAMES; do + lib_file="$LIB_DIR/$lib_filename" + printf "$script_name sourced lib file: %s\n" "$lib_file" + done +fi + +# determine the metadata source +if $md_query_mode; then + if [ -z "$mdq_base_url" ]; then + echo "ERROR: $script_name: option -u requires an argument" >&2 + exit 2 + fi + $verbose_mode && printf "$script_name using base URL: %s\n" "$mdq_base_url" +elif $md_file_mode; then + if [ -z "$md_path" ]; then + echo "ERROR: $script_name: option -f requires an argument" >&2 + exit 2 + fi + if [ ! -f "$md_path" ]; then + echo "ERROR: $script_name: file does not exist: $md_path" >&2 + exit 2 + fi + $verbose_mode && printf "$script_name using metadata file: %s\n" "$md_path" +else + echo "ERROR: $script_name: one of options -u or -f required" >&2 + exit 2 +fi + +# redirect stdout and stderr to the bit bucket +$quiet_mode && exec 1>/dev/null +$quiet_mode && exec 2>/dev/null + +# check consistency of timeout options +if [ -n "$max_time" -a -z "$connect_timeout" ]; then + echo "ERROR: $script_name: the -m option requires the presence of the -t option" >&2 + exit 2 +fi + +# set default connect timeout if necessary +if [ -z "$connect_timeout" ]; then + connect_timeout=$connect_timeout_default + local_opts="$local_opts -t $connect_timeout" +else + if [ "$connect_timeout" -le 0 ] ; then + echo "ERROR: $script_name: connect timeout ($connect_timeout) must be a positive integer" >&2 + exit 2 + fi +fi + +# compute max time if necessary +if [ -z "$max_time" ]; then + max_time=$(( connect_timeout + 2 )) + local_opts="$local_opts -m $max_time" +else + if [ "$max_time" -le "$connect_timeout" ]; then + echo "ERROR: $script_name: max time ($max_time) must be greater than the connect timeout ($connect_timeout)" >&2 + exit 2 + fi +fi + +# check maximum number of redirects +if [ -z "$max_redirs" ]; then + max_redirs=$max_redirs_default + local_opts="$local_opts -r $max_redirs" +fi + +if $verbose_mode; then + printf "$script_name using connect timeout: %d secs\n" $connect_timeout + printf "$script_name using max time: %d secs\n" $max_time + printf "$script_name using max redirects: %d\n" $max_redirs +fi + +# determine the entityID +shift $(( OPTIND - 1 )) +if [ $# -ne 1 ]; then + echo "ERROR: $script_name: wrong number of arguments: $# (1 required)" >&2 + exit 2 +fi +entityID="$1" +if [ -z "$entityID" ] ; then + echo "ERROR: $script_name: empty string" >&2 + exit 2 +fi +$verbose_mode && echo "$script_name using entityID $entityID" + +##################################################################### +# Initialization +##################################################################### + +# determine temporary directory +if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + # use system temporary directory (remove trailing slash) + TMP_DIR="${TMPDIR%%/}/probe_saml_idp_$$" + $verbose_mode && printf "$script_name using temp dir: %s\n" "$TMP_DIR" +else + # create temporary directory + tmp_dir="$( make_temp_file -d )" + if [ ! -d "$tmp_dir" ] ; then + printf "ERROR: $script_name unable to create temporary dir\n" >&2 + exit 2 + fi + # use temporary directory (remove trailing slash) + TMP_DIR="${tmp_dir%%/}/probe_saml_idp_$$" + $verbose_mode && printf "$script_name creating temp dir: %s\n" "$TMP_DIR" +fi + +# create temporary directory if necessary +if [ ! -d "$TMP_DIR" ]; then + /bin/mkdir "$TMP_DIR" + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $script_name failed to create tmp dir ($exit_status) $TMP_DIR" >&2 + exit 2 + fi +fi + +# create temporary subdirectories if necessary +for http_binding in Redirect POST POST-SimpleSign; do + if [ ! -d "$TMP_DIR/$http_binding" ]; then + /bin/mkdir "$TMP_DIR/$http_binding" + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $script_name failed to create tmp dir ($exit_status) $TMP_DIR/$http_binding" >&2 + exit 2 + fi + fi +done + +##################################################################### +# Main processing +##################################################################### + +# get entity metadata +entityDescriptor=$( getEntityFromServer -d "$TMP_DIR" -u "$mdq_base_url" $entityID ) +exit_status=$? +if [ "$exit_status" -ne 0 ]; then + echo "ERROR: $script_name: unable to obtain metadata for entityID: $entityID" >&2 + exit 3 +fi + +# extract the registrar ID from the entity descriptor +registrarID=$( echo "$entityDescriptor" \ + | $_GREP -F -m 1 ' registrationAuthority=' \ + | $_SED -e 's/^.* registrationAuthority="\([^"]*\)".*$/\1/' +) + +# compute all SSO endpoints +endpoints=$( echo "$entityDescriptor" \ + | $_GREP -E '<(md:)?SingleSignOnService ' +) + +# iterate over the SAML2 browser-facing endpoints +for http_binding in Redirect POST POST-SimpleSign; do + + # compute the endpoint + endpoint=$( echo "$endpoints" \ + | $_GREP -F -m 1 ' Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-'$http_binding'"' + ) + [ -z "$endpoint" ] && continue + + # compute the endpoint location and binding + location=$( echo "$endpoint" \ + | $_SED -e 's/^.* Location="\([^"]*\)".*$/\1/' + ) + binding=$( echo "$endpoint" \ + | $_SED -e 's/^.* Binding="\([^"]*\)".*$/\1/' + ) + $verbose_mode && printf "$script_name probing endpoint with Location=\"%s\" and Binding=\"%s\"\n" "$location" "$binding" + + # construct the SAML message + saml_message=$( construct_SAML2_AuthnRequest $location ) + exit_status=$? + if [ "$exit_status" -ne 0 ]; then + echo "ERROR: $script_name: construct_SAML2_AuthnRequest failed ($exit_status)" >&2 + exit 3 + fi + + # probe the endpoint + output=$( probe_saml2_idp_endpoint -v \ + -t $connect_timeout -m $max_time -r $max_redirs \ + -T "$TMP_DIR/$http_binding" \ + $location $binding "$saml_message" + ) + exit_status=$? + if [ "$exit_status" -ne 0 ]; then + echo "ERROR: $script_name: probe_saml2_idp_endpoint failed ($exit_status)" >&2 + exit 3 + fi + echo "$output $entityID $registrarID" + +done diff --git a/install.sh b/install.sh index d8c57c3..f6ef27c 100755 --- a/install.sh +++ b/install.sh @@ -93,6 +93,7 @@ while read script_file; do fi done <&2 + return 2 + fi + + # user agent + local script_version="0.5" + local user_agent_string="SAML2 IdP Endpoint Probe ${script_version}" + + # command-line options + local verbose_mode=false + local local_opts + local connect_timeout + local max_time + local max_redirs + local tmp_dir + + # command-line arguments + local idp_endpoint_binding + local idp_endpoint_location + local saml_message + + # temporary files + local tmp_log_file + local header_file + local response_file + local cookie_jar_file + local curl_trace_file + local deflated_message_file + local base64_encoded_message_file + + local exit_status + local base64_encoded_message + local percent_encoded_message + local protocol_url + + local curl_opts + local curl_output + local curl_error_code + + ################################################################### + # Process command-line options and arguments. + ################################################################### + + local opt + local OPTARG + local OPTIND + while getopts ":vt:m:r:T:" opt; do + case $opt in + v) + verbose_mode=true + local_opts="$local_opts -$opt" + ;; + t) + connect_timeout="$OPTARG" + ;; + m) + max_time="$OPTARG" + ;; + r) + max_redirs="$OPTARG" + ;; + T) + tmp_dir="$OPTARG" + ;; + \?) + echo "ERROR: $FUNCNAME: Unrecognized option: -$OPTARG" >&2 + return 2 + ;; + :) + echo "ERROR: $FUNCNAME: Option -$OPTARG requires an argument" >&2 + return 2 + ;; + esac + done + + if [ -z "$connect_timeout" ]; then + echo "ERROR: $FUNCNAME: connection timeout (option -t) required" >&2 + return 2 + fi + + if [ -z "$max_time" ]; then + echo "ERROR: $FUNCNAME: max time (option -m) required" >&2 + return 2 + fi + + if [ -z "$max_redirs" ]; then + echo "ERROR: $FUNCNAME: max redirects (option -r) required" >&2 + return 2 + fi + + # check for a temporary directory + if [ -z "$tmp_dir" ]; then + echo "ERROR: $FUNCNAME: temporary directory (option -T) required" >&2 + return 2 + fi + if [ ! -d "$tmp_dir" ]; then + echo "ERROR: $FUNCNAME: temporary directory does not exist: $tmp_dir" >&2 + return 2 + fi + + # make sure there are the correct number of command-line arguments + shift $(( OPTIND - 1 )) + if [ $# -ne 3 ]; then + echo "ERROR: $FUNCNAME: incorrect number of arguments: $# (3 required)" >&2 + return 2 + fi + idp_endpoint_location="$1" + idp_endpoint_binding="$2" + saml_message="$3" + + # check the binding + if [ "$idp_endpoint_binding" != "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" ] && \ + [ "$idp_endpoint_binding" != "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" ] && \ + [ "$idp_endpoint_binding" != "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" ]; then + echo "ERROR: $FUNCNAME: unrecognized binding: $idp_endpoint_binding" >&2 + return 2 + fi + + ################################################################### + # Initialization complete. + ################################################################### + + # temporary log file + tmp_log_file="$tmp_dir/${FUNCNAME}_log" + echo "$FUNCNAME using temporary directory: $tmp_dir" > "$tmp_log_file" + + # log input data + printf "$FUNCNAME using connection timeout (option -t): %d\n" "$connect_timeout" >> "$tmp_log_file" + printf "$FUNCNAME using max time (option -m): %d\n" "$max_time" >> "$tmp_log_file" + printf "$FUNCNAME using max redirects (option -r): %d\n" "$max_redirs" >> "$tmp_log_file" + printf "$FUNCNAME using IdP endpoint binding: %s\n" "$idp_endpoint_binding" >> "$tmp_log_file" + printf "$FUNCNAME using IdP endpoint location: %s\n" "$idp_endpoint_location" >> "$tmp_log_file" + printf "$FUNCNAME using SAML message (flattened): %s\n" "$( echo $saml_message | /usr/bin/tr -d '\n\r' )" >> "$tmp_log_file" + + # temporary files + header_file="${tmp_dir}/idp_http_header.txt" + printf "$FUNCNAME using IdP header file: %s\n" "$header_file" >> "$tmp_log_file" + response_file="${tmp_dir}/idp_http_response.html" + printf "$FUNCNAME using IdP response file: %s\n" "$response_file" >> "$tmp_log_file" + cookie_jar_file="${tmp_dir}/idp_cookie_jar.txt" + printf "$FUNCNAME using IdP cookie file: %s\n" "$cookie_jar_file" >> "$tmp_log_file" + + # compute curl command-line options + curl_opts="--silent --show-error" + if $verbose_mode; then + curl_trace_file="${tmp_dir}/curl_trace.txt" + curl_opts="${curl_opts} --trace-ascii $curl_trace_file" + fi + curl_opts="${curl_opts} --connect-timeout $connect_timeout --max-time $max_time" + curl_opts="${curl_opts} --insecure --tlsv1" + curl_opts="${curl_opts} --location --max-redirs $max_redirs" + curl_opts="${curl_opts} --cookie-jar $cookie_jar_file --cookie $cookie_jar_file" + curl_opts="${curl_opts} --dump-header $header_file" + curl_opts="${curl_opts} --output $response_file" + + ################################################################### + # Compute the protocol URL. + ################################################################### + + # HTTP-Redirect or HTTP-POST? + if [ "$idp_endpoint_binding" = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" ]; then + + # Note: The deflated message is stored in a file. + # It is not stored in a variable since the echo + # command does not operate safely on binary data. + + deflated_message_file="${tmp_dir}/saml_message.xml.deflate" + printf "$FUNCNAME using deflated message file: %s\n" "$deflated_message_file" >> "$tmp_log_file" + + # deflate the SAML message + deflate $local_opts -T $tmp_dir "$saml_message" > "$deflated_message_file" + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: failed to deflate the message ($exit_status)" >&2 + return 3 + fi + + # base64-encode the deflated message + base64_encoded_message=$( /usr/bin/base64 "$deflated_message_file" ) + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: failed to base64-encode the deflated message ($exit_status)" >&2 + return 3 + fi + printf "$FUNCNAME computed base64-encoded message: %s\n" "$base64_encoded_message" >> "$tmp_log_file" + + # percent-encode the base64-encoded, deflated SAML message + percent_encoded_message=$( percent_encode "$base64_encoded_message" ) + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: failed to percent-encode message ($exit_status)" >&2 + return 3 + fi + printf "$FUNCNAME computed percent-encoded message: %s\n" "$percent_encoded_message" >> "$tmp_log_file" + + # construct the URL subject to the SAML2 HTTP-Redirect binding + protocol_url=${idp_endpoint_location}?SAMLRequest=$percent_encoded_message + printf "$FUNCNAME computed protocol URL: %s\n" "$protocol_url" >> "$tmp_log_file" + + else + + base64_encoded_message_file="${tmp_dir}/saml_message.xml.base64" + printf "$FUNCNAME using encoded message file: %s\n" "$base64_encoded_message_file" >> "$tmp_log_file" + + # base64-encode the SAML message + echo -n "$saml_message" | /usr/bin/base64 > "$base64_encoded_message_file" + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: failed to base64-encode the message ($exit_status)" >&2 + return 3 + fi + + # in the case of HTTP-POST, the protocol URL IS the endpoint location + protocol_url=$idp_endpoint_location + printf "$FUNCNAME computed protocol URL: %s\n" "$protocol_url" >> "$tmp_log_file" + + curl_opts="${curl_opts} --data-urlencode SAMLRequest@$base64_encoded_message_file" + + fi + + printf "$FUNCNAME using curl opts: %s\n" "$curl_opts" >> "$tmp_log_file" + + ################################################################### + # Probe the IdP endpoint. + ################################################################### + + # transmit the request to the IdP + curl_output=$( /usr/bin/curl ${curl_opts} \ + --user-agent "$user_agent_string" \ + --write-out 'redirects:%{num_redirects};response:%{http_code};dns:%{time_namelookup};tcp:%{time_connect};ssl:%{time_appconnect};total:%{time_total}' \ + "$protocol_url" + ) + curl_error_code=$? + + # only the last line of output is processed further + curl_output=$(echo "$curl_output" | /usr/bin/tail -n 1) + printf "$FUNCNAME output: %s %s %s %s\n" "$curl_error_code $curl_output $idp_endpoint_location $idp_endpoint_binding" >> "$tmp_log_file" + echo "$curl_error_code $curl_output $idp_endpoint_location $idp_endpoint_binding" + + return 0 +} + +####################################################################### +# +# This function transmits a SAML message to an IdP endpoint location +# via the Shibboleth 1.3 AuthnRequest protocol. The latter is a +# proprietary (but widely used) protocol for IdPs that support +# the SAML1 Web Browser SSO profile. +# +# Usage: +# probe_shibboleth_sso_endpoint [-v] \ +# -t CONNECT_TIME -m MAX_TIME \ +# -r MAX_REDIRS \ +# -T TMP_DIR \ +# IDP_ENDPOINT_LOCATION [IDP_ENDPOINT_BINDING] +# where +# IDP_ENDPOINT_LOCATION and IDP_ENDPOINT_BINDING are the +# Location and Binding XML attribute values of a particular +# browser-facing endpoint at the IdP. This script probes +# an endpoint with binding URI: +# +# urn:mace:shibboleth:1.0:profiles:AuthnRequest +# +# Since only one binding is recognized by this script, the +# binding URI is an optional command-line argument. +# +# The output of this script consists of a single line with four +# space-separated fields: +# +# 1. curl error code +# 2. curl output string +# 3. IdP endpoint location +# 4. IdP endpoint binding +# +# The function records the details of the various processing steps +# and the resulting HTTP transaction in files stored in the given +# temporary directory. If the -v option is specified on the command +# line, a curl trace of the transaction is also provided. In the +# temporary directory, see this log file for details: +# +# probe_shibboleth_sso_endpoint_log +# +####################################################################### +probe_shibboleth_sso_endpoint () { + + # check global env vars + if [ -z "$SAML1_SP_ENTITY_ID" ]; then + echo "ERROR: $FUNCNAME requires env var SAML1_SP_ENTITY_ID" >&2 + return 2 + fi + if [ -z "$SAML1_SP_ACS_URL" ]; then + echo "ERROR: $FUNCNAME requires env var SAML1_SP_ACS_URL" >&2 + return 2 + fi + # make the binding optional + if [ -z "$SAML1_SP_ACS_BINDING" ]; then + echo "ERROR: $FUNCNAME requires env var SAML1_SP_ACS_BINDING" >&2 + return 2 + fi + + # external dependency + if [ "$(type -t percent_encode)" != function ]; then + echo "ERROR: $FUNCNAME: function percent_encode not found" >&2 + return 2 + fi + + # user agent + local script_version="0.2" + local user_agent_string="SAML1 IdP Endpoint Probe ${script_version}" + + # command-line options + local verbose_mode=false + local local_opts + local connect_timeout + local max_time + local max_redirs + local tmp_dir + + # command-line arguments + local idp_shibboleth_sso_binding + local idp_shibboleth_sso_location + + # temporary files + local tmp_log_file + local header_file + local response_file + local cookie_jar_file + local curl_trace_file + + local exit_status + local encoded_entityid + local encoded_acs_url + local protocol_url + + local curl_opts + local curl_output + local curl_error_code + + ################################################################### + # Process command-line options and arguments. + ################################################################### + + local opt + local OPTARG + local OPTIND + while getopts ":vt:m:r:T:" opt; do + case $opt in + v) + verbose_mode=true + local_opts="$local_opts -$opt" + ;; + t) + connect_timeout="$OPTARG" + ;; + m) + max_time="$OPTARG" + ;; + r) + max_redirs="$OPTARG" + ;; + T) + tmp_dir="$OPTARG" + ;; + \?) + echo "ERROR: $FUNCNAME: Unrecognized option: -$OPTARG" >&2 + return 2 + ;; + :) + echo "ERROR: $FUNCNAME: Option -$OPTARG requires an argument" >&2 + return 2 + ;; + esac + done + + if [ -z "${connect_timeout}" ]; then + echo "ERROR: $FUNCNAME: connection timeout (option -t) required" >&2 + return 2 + fi + + if [ -z "${max_time}" ]; then + echo "ERROR: $FUNCNAME: max time (option -m) required" >&2 + return 2 + fi + + if [ -z "${max_redirs}" ]; then + echo "ERROR: $FUNCNAME: max redirects (option -r) required" >&2 + return 2 + fi + + # check for a temporary directory + if [ -z "$tmp_dir" ]; then + echo "ERROR: $FUNCNAME: temporary directory (option -T) required" >&2 + return 2 + fi + if [ ! -d "$tmp_dir" ]; then + echo "ERROR: $FUNCNAME: temporary directory does not exist: $tmp_dir" >&2 + return 2 + fi + + # make sure there are the correct number of command-line arguments + shift $(( OPTIND - 1 )) + if [ $# -lt 1 ]; then + echo "ERROR: $FUNCNAME: too few arguments: $# (at least 1 required)" >&2 + return 2 + fi + if [ $# -gt 2 ]; then + echo "ERROR: $FUNCNAME: too many arguments: $# (at most 2 required)" >&2 + return 2 + fi + + # capture the command-line argument(s) + if [ $# -eq 1 ]; then + idp_shibboleth_sso_location="$1" + idp_shibboleth_sso_binding=urn:mace:shibboleth:1.0:profiles:AuthnRequest + else + idp_shibboleth_sso_location="$1" + idp_shibboleth_sso_binding="$2" + # check the binding + if [ "$idp_shibboleth_sso_binding" != "urn:mace:shibboleth:1.0:profiles:AuthnRequest" ]; then + echo "ERROR: $FUNCNAME: unrecognized binding: $idp_shibboleth_sso_binding" >&2 + return 2 + fi + fi + + ################################################################### + # Initialization complete. + ################################################################### + + # temporary log file + tmp_log_file="$tmp_dir/${FUNCNAME}_log" + echo "$FUNCNAME using temporary directory: $tmp_dir" > "$tmp_log_file" + + # log global env vars + printf "$FUNCNAME using SP with entityID: %s\n" "$SAML1_SP_ENTITY_ID" >> "$tmp_log_file" + printf "$FUNCNAME using SP ACS URL: %s\n" "$SAML1_SP_ACS_URL" >> "$tmp_log_file" + printf "$FUNCNAME using SP ACS Binding: %s\n" "$SAML1_SP_ACS_BINDING" >> "$tmp_log_file" + + # log input data + printf "$FUNCNAME using connection timeout (option -t): %d\n" "$connect_timeout" >> "$tmp_log_file" + printf "$FUNCNAME using max time (option -m): %d\n" "$max_time" >> "$tmp_log_file" + printf "$FUNCNAME using max redirects (option -r): %d\n" "$max_redirs" >> "$tmp_log_file" + printf "$FUNCNAME using IdP endpoint location: %s\n" "$idp_shibboleth_sso_location" >> "$tmp_log_file" + printf "$FUNCNAME using IdP endpoint binding: %s\n" "$idp_shibboleth_sso_binding" >> "$tmp_log_file" + + # temporary files + header_file="${tmp_dir}/idp_http_header.txt" + printf "$FUNCNAME using IdP header file: %s\n" "$header_file" >> "$tmp_log_file" + response_file="${tmp_dir}/idp_http_response.html" + printf "$FUNCNAME using IdP response file: %s\n" "$response_file" >> "$tmp_log_file" + cookie_jar_file="${tmp_dir}/idp_cookie_jar.txt" + printf "$FUNCNAME using IdP cookie file: %s\n" "$cookie_jar_file" >> "$tmp_log_file" + + # compute curl command-line options + curl_opts="--silent --show-error" + if $verbose_mode; then + curl_trace_file="${tmp_dir}/curl_trace.txt" + curl_opts="${curl_opts} --trace-ascii $curl_trace_file" + fi + curl_opts="${curl_opts} --connect-timeout $connect_timeout --max-time $max_time" + curl_opts="${curl_opts} --insecure --tlsv1" + curl_opts="${curl_opts} --location --max-redirs $max_redirs" + curl_opts="${curl_opts} --cookie-jar $cookie_jar_file --cookie $cookie_jar_file" + curl_opts="${curl_opts} --dump-header $header_file" + curl_opts="${curl_opts} --output $response_file" + printf "$FUNCNAME using curl opts: %s\n" "$curl_opts" >> "$tmp_log_file" + + ################################################################### + # Compute the protocol URL. + ################################################################### + + # percent-encode the SP entityID + encoded_entityid=$( percent_encode "$SAML1_SP_ENTITY_ID" ) + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: failed to percent-encode SP entityID ($exit_status)" >&2 + return 3 + fi + printf "$FUNCNAME encoded SP entityID: %s\n" "$encoded_entityid" >> "$tmp_log_file" + + # percent-encode the SP AssertionConsumerService location + encoded_acs_url=$( percent_encode "$SAML1_SP_ACS_URL" ) + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: failed to percent-encode ACS location ($exit_status)" >&2 + return 3 + fi + printf "$FUNCNAME encoded SP ACS URL: %s\n" "$encoded_acs_url" >> "$tmp_log_file" + + # construct the URL subject to the Shibboleth 1.3 AuthnRequest protocol + protocol_url="${idp_shibboleth_sso_location}?providerId=${encoded_entityid}&shire=${encoded_acs_url}&target=cookie" + printf "$FUNCNAME computed protocol URL: %s\n" "$protocol_url" >> "$tmp_log_file" + + ################################################################### + # Probe the IdP endpoint. + ################################################################### + + # transmit the request to the IdP + curl_output=$( /usr/bin/curl ${curl_opts} \ + --user-agent "$user_agent_string" \ + --write-out 'redirects:%{num_redirects};response:%{http_code};dns:%{time_namelookup};tcp:%{time_connect};ssl:%{time_appconnect};total:%{time_total}' \ + "$protocol_url" + ) + curl_error_code=$? + + # only the last line of output is processed further + curl_output=$(echo "$curl_output" | /usr/bin/tail -n 1) + printf "$FUNCNAME output: %s %s %s %s\n" "$curl_error_code $curl_output $idp_shibboleth_sso_location $idp_shibboleth_sso_binding" >> "$tmp_log_file" + echo "$curl_error_code $curl_output $idp_shibboleth_sso_location $idp_shibboleth_sso_binding" + + return 0 +} + +####################################################################### +# +# A native BASH implementation of DEFLATE compression (RFC 1951) +# +# Usage: +# deflate [-v] -T TMP_DIR STRING_TO_DEFLATE +# where +# TMP_DIR is a temporary working directory +# STRING_TO_DEFLATE is the actual string to be deflated +# +# This implementation leverages the fact that the popular tool gzip +# relies on DEFLATE compression at its core. The trick is to invoke +# 'gzip --no-name', which compresses its input without storing a +# filename or timestamp in the output. This yields a (fixed) 10-byte +# header along with the usual 8-byte trailer, both of which are +# stripped from the output of the gzip command by this function. The +# end result is a DEFLATE compressed stream of bytes. +# +# See: http://stackoverflow.com/questions/27066133/how-to-create-bare-deflate-stream-from-file-in-linux +# +# Warning: This function outputs binary data. To use it interactively, +# it's probably best to base64-encode the deflated string: +# $ deflate -T $TMPDIR "hello world" | /usr/bin/base64 +# y0jNyclXKM8vykkBAA== +# +####################################################################### +deflate () { + + # external dependency + if [ "$COMMAND_PATHS" != true ]; then + echo "ERROR: $FUNCNAME: global command paths not found" >&2 + return 2 + fi + + local verbose_mode=false + local tmp_dir + local tmp_log_file + local string_to_deflate + local n + + # temporary files + local zipfile + local headerfile + local trailerfile + local noheaderfile + local strippedfile + + local opt + local OPTARG + local OPTIND + while getopts ":vT:" opt; do + case $opt in + v) + verbose_mode=true + ;; + T) + tmp_dir="$OPTARG" + ;; + \?) + echo "ERROR: $FUNCNAME: Unrecognized option: -$OPTARG" >&2 + return 2 + ;; + :) + echo "ERROR: $FUNCNAME: Option -$OPTARG requires an argument" >&2 + return 2 + ;; + esac + done + + # a temporary directory is required + if [ -z "$tmp_dir" ]; then + echo "ERROR: $FUNCNAME: no temporary directory specified" >&2 + return 2 + fi + if [ ! -d "$tmp_dir" ]; then + echo "ERROR: $FUNCNAME: directory does not exist: $tmp_dir" >&2 + return 2 + fi + tmp_log_file="$tmp_dir/${FUNCNAME}_log" + $verbose_mode && echo "$FUNCNAME using temporary directory $tmp_dir" > "$tmp_log_file" + + # determine the URL location + shift $(( OPTIND - 1 )) + if [ $# -ne 1 ]; then + echo "ERROR: $FUNCNAME: wrong number of arguments: $# (1 required)" >&2 + return 2 + fi + string_to_deflate="$1" + if [ -z "$string_to_deflate" ] ; then + echo "ERROR: $FUNCNAME: empty string" >&2 + return 2 + fi + $verbose_mode && echo "$FUNCNAME deflating string $string_to_deflate" >> "$tmp_log_file" + + zipfile=$tmp_dir/${FUNCNAME}_junk.gz + headerfile=$tmp_dir/${FUNCNAME}_junk.gz.header + trailerfile=$tmp_dir/${FUNCNAME}_junk.gz.trailer + noheaderfile=$tmp_dir/${FUNCNAME}_junk.gz.no-header + strippedfile=$tmp_dir/${FUNCNAME}_junk.gz.stripped + if $verbose_mode; then + echo "$FUNCNAME using temporary file $zipfile" >> "$tmp_log_file" + echo "$FUNCNAME using temporary file $headerfile" >> "$tmp_log_file" + echo "$FUNCNAME using temporary file $trailerfile" >> "$tmp_log_file" + echo "$FUNCNAME using temporary file $noheaderfile" >> "$tmp_log_file" + echo "$FUNCNAME using temporary file $strippedfile" >> "$tmp_log_file" + fi + + # compress with no filename or timestamp stored in the output, + # which yields a (fixed) 10-byte header and the usual 8-byte trailer. + echo -n "$string_to_deflate" | $_GZIP -q --no-name > $zipfile + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: gzip failed ($exit_status)" >&2 + return 3 + fi + + # strip (and save) the 10-byte header + /bin/cat $zipfile | ( /bin/dd of=$headerfile bs=1 count=10 2>/dev/null; /bin/cat > $noheaderfile ) + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: unable to strip header ($exit_status)" >&2 + return 3 + fi + + # compute the size (in bytes) of the remaining file + n=$( /bin/cat $noheaderfile | /usr/bin/wc -c ) + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: unable to compute file size ($exit_status)" >&2 + return 3 + fi + + # strip (and save) the 8-byte trailer + /bin/cat $noheaderfile | ( /bin/dd of=$strippedfile bs=1 count=$[ n - 8 ] 2>/dev/null; /bin/cat > $trailerfile ) + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: unable to strip trailer ($exit_status)" >&2 + return 3 + fi + + # sanity check + /bin/cat $headerfile $strippedfile $trailerfile | /usr/bin/diff -q $zipfile - >&2 + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: diff failed ($exit_status)" >&2 + return 3 + fi + + # return the DEFLATE-compressed input string + /bin/cat $strippedfile + +} + +####################################################################### +# This function is intentionally not documented +####################################################################### +construct_SAML2_AuthnRequest () { + + # check global env vars + if [ -z "$SAML2_SP_ENTITY_ID" ]; then + echo "ERROR: $FUNCNAME requires env var SAML2_SP_ENTITY_ID" >&2 + return 2 + fi + if [ -z "$SAML2_SP_ACS_URL" ]; then + echo "ERROR: $FUNCNAME requires env var SAML2_SP_ACS_URL" >&2 + return 2 + fi + if [ -z "$SAML2_SP_ACS_BINDING" ]; then + echo "ERROR: $FUNCNAME requires env var SAML2_SP_ACS_BINDING" >&2 + return 2 + fi + + local message_id + local exit_status + local dateStr + + # input arguments + local idp_sso_location + + # make sure there are the correct number of command-line arguments + #shift $(( OPTIND - 1 )) + if [ $# -ne 1 ]; then + echo "ERROR: $FUNCNAME: incorrect number of arguments: $# (1 required)" >&2 + return 2 + fi + idp_sso_location="$1" + + # compute value of ID XML attribute + # (40 bytes of pseudo-random alphanumeric characters) + message_id=$( LC_CTYPE=C /usr/bin/tr -dc '[:alnum:]' < /dev/urandom \ + | /bin/dd bs=4 count=10 2>/dev/null + ) + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: failed to produce message ID ($exit_status)" >&2 + return 3 + fi + + # compute value of IssueInstant XML attribute + # (claim: use of /bin/date compatible on Mac OS and GNU/Linux) + dateStr=$( /bin/date -u +%Y-%m-%dT%TZ ) + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "ERROR: $FUNCNAME: failed to produce dateTime string ($exit_status)" >&2 + return 3 + fi + + cat <<- SAMLAuthnRequest + + ${SAML2_SP_ENTITY_ID} + + + SAMLAuthnRequest + +}