Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 641 lines (572 sloc) 17.4 KB
#!/usr/bin/env bash
# testrad -- automates RADIUS testing between domains
#
# (c) 2014-2016 Mantas Mikulėnas <grawity@gmail.com>
# Released under the MIT License (dist/LICENSE.mit)
#
# Requires 'eapol_test' (from wpa_supplicant) and 'radtest' to be in path.
. lib.bash || exit
lib_config[opt_width]=24
usage() {
echo "Usage: $progname PROFILE [via SERVER] [options]"
echo ""
echo_opt "PROFILE" "Use given profile config block (or 'none')"
echo ""
echo "Server:"
echo_opt "via SERVERPROFILE" "Use given server config block"
echo_opt "host ADDRESS" "Set RADIUS host name or address"
echo_opt "port PORT" "Set RADIUS authentication port"
echo_opt "proto ip4|ip6" "Force IP protocol version"
echo_opt "secret SECRET" "Set RADIUS secret"
echo ""
echo "Mechanism:"
echo_opt "eap|phase1 MECH" "Tunnel inside an EAP mechanism (peap, tls, ttls)"
echo_opt "mech|phase2 MECH" "Set the main mechanism (pap, mschap, gtc)"
echo ""
echo "Outer identity:"
echo_opt "anon" "Use '@realm' as outer (anonymous) identity"
echo_opt "outer[-user] ..." "Set outer (anonymous) identity"
echo ""
echo "Inner identity:"
echo_opt "user ..." "Set main (inner) username"
echo_opt "pass ..." "Set main (inner) password"
echo_opt "[tls-]cert PATH" "Set client-auth certificate for EAP-TLS"
echo_opt "[tls-]key PATH" "Set client-auth private key for EAP-TLS"
echo ""
echo "Server verification:"
echo_opt "[tls-]ca PATH" "Verify server against given CA"
echo_opt "[tls-]ca system" "Verify server against default system CAs"
echo_opt "tls-hostname DOMAIN" "Require exact hostname (alias for domain-match)"
echo_opt "tls-hostname *.DOMAIN" "Require domain suffix (alias for domain-suffix-match)"
echo ""
echo_opt "domain-match ..." "Check certificate domain (either CN or SAN)"
echo_opt "domain-suffix-match ..." "Check certificate domain (either CN or SAN)"
echo_opt "subject-match ..." "Check certificate Subject [deprecated!]"
echo_opt "altsubject-match ..." "Check certificate subjectAltName [deprecated!]"
echo ""
echo "Phase 1 TLS options:"
echo_opt "tls v1.3" "Enable TLS v1.3 (disabled by default in wpa_supplicant)"
echo_opt "tls no-v1.2" "Disable TLS v1.2 (for broken servers)"
echo_opt "tls no-v1.1" "Disable TLS v1.1 (for really broken servers)"
echo_opt "tls no-v1.0" "Disable TLS v1.0 (for epicly broken servers)"
echo_opt "tls ..." "Manually add a phase1=\"...\" option"
echo ""
echo "Miscellaneous:"
echo_opt "show-config" "Dump generated eapol_test config and exit"
echo_opt "inherit PROFILE" "In testrad.conf.sh, import given profile as base"
echo_opt "-4|-6" "Force IPv4 or IPv6 (alias for 'proto ip4|ip6')"
echo_opt "-o ..." "Manually add a config setting for eapol_test"
echo_opt "-O PATH" "Save phase 1 TLS certificate chain to file"
echo_opt "-x PATH" "Use a different eapol_test executable"
echo_opt "--help TOPIC" "Show additional help text"
echo ""
echo "Many options translate directly to wpa_supplicant settings."
echo "Both GNU \"--opt ARG\" and iproute \"opt ARG\" formats are accepted."
echo ""
echo "Use \"--help TOPIC\" for additional information. Available topics:"
echo_opt "config" "Configuration files"
echo_opt "phase1" "Supported phase-1 EAP mechanisms"
echo_opt "phase2" "Supported phase-2 mechanisms"
}
help() {
if [[ $(type -t "help_$1") == function ]]; then
"help_$1"
elif [[ $1 ]]; then
die "unknown help topic '$1'"
else
usage
fi
}
help_eap() { help_phase1; }
help_mech() { help_phase2; }
help_phase1() {
echo "Supported phase-1 (outer) EAP mechanisms:"
echo ""
echo " EAP SETTING SUPPORTED PHASE2 SETTINGS"
echo " ----------- ------------------------------------------"
echo " NONE plain pap, chap, mschap"
echo " GTC -"
echo " MD5 -"
echo " MSCHAPV2 -"
echo " OTP -"
echo " PEAP EAP-based (eap-gtc, eap-md5, eap-mschapv2...)"
echo " TLS -"
echo " TTLS everything"
echo ""
echo "Notes:"
echo ""
echo " * 'eap none' uses radtest, so the mechanism support is somewhat limited."
echo ""
echo " * All other 'eap foo' settings map directly to 'eap=FOO' in wpa_supplicant."
echo " For PEAP and TTLS, if phase2 is unset then all mechanisms will be accepted."
echo ""
echo " * Pay attention to MSCHAP invocation:"
echo " - 'phase1 none phase2 mschap' will give you MSCHAPv1 via radtest"
echo " - 'phase1 mschapv2 phase2 none' will give you EAP-MSCHAPv2 via eapol_test"
}
help_phase2() {
echo "Supported phase-2 (inner) mechanisms for PEAP & TTLS:"
echo ""
echo " MECH SETTING NAME EAP=PEAP EAP=TTLS"
echo " ------------ -------------- -------------- ----------------"
echo " pap PAP - auth=PAP"
echo " chap CHAP - auth=CHAP"
echo " mschap MSCHAPv1 - auth=MSCHAP"
echo " mschapv2 MSCHAPv2 - auth=MSCHAPV2"
echo " eap-gtc GTC auth=GTC autheap=GTC"
echo " eap-md5 MD5 auth=MD5 autheap=MD5"
echo " eap-mschapv2 MSCHAPv2 auth=MSCHAPV2 autheap=MSCHAPV2"
echo ""
echo "Notes:"
echo ""
echo " * TTLS supports all mechanisms, while PEAP can only carry EAP-based ones."
echo ""
echo " * PAP is not an EAP mechanism – its equivalent for PEAP is EAP-GTC."
echo ""
echo " * MSCHAPv2 exists in both raw & EAP versions, resulting in 3 combinations."
}
help_config() {
local c=$path_config/testrad.conf.sh
echo "Configurations are loaded from ${c/#"$HOME"/"~"}:"
echo ""
echo " server_foo=("
echo " host radius.example.com"
echo " secret quux"
echo " )"
echo " profile_foo=("
echo " # 'via foo' is implied"
echo " user test@example.com"
echo " pass testing123"
echo " )"
echo " profile_foo_eap=("
echo " inherit foo"
echo " eap peap"
echo " )"
echo ""
echo "In command line, the first parameter is *always* a profile name."
echo "Use 'testrad none ...' if no profile needs to be loaded."
}
declare -- ip_version host port secret eap mech phase2
declare -- identity outer_id password
declare -- tls_cert tls_key tls_ca save_chain
declare -a tls_opts phase1_opts
declare -a ecfg_full ecfg_extra
declare -A ecfg_values ecfg_seen
eapol_bin="eapol_test"
# parse config
debug "loading profiles from '$path_config/testrad.conf.sh'"
. "$path_config/testrad.conf.sh" || die "could not load configuration"
# parse arguments
declare -- profile=
declare -i depth=0
load_conf() {
local type=$1 name=${2//-/_}
local var="${type}_${name}[@]"
set -- "${!var}"
if [[ $name == none ]]; then
debug "accepting '$name' as dummy $type"
return
elif (( ! $# )); then
err "$type '$name' not found"
return
elif (( depth > 5 )); then
die "profile recursion limit exceeded"
else
(( ++depth ))
debug "($depth) parsing $var"
parse_args "$@"
debug "($depth) finished $var"
(( depth-- ))
fi
}
parse_args() {
debug "current load type '$type', name '$name'"
while (( $# )); do
if [[ $1 == --help && ! $type ]]; then
help "$2"; lib::exit
elif [[ ! $profile && ! $type ]]; then
debug "<$type/$name:$#> accepting first arg '$1' as profile name"
profile=$1
debug "<$type/$name:$#> loading profile config for '$profile'"
load_conf profile "$profile"
else
debug "<$type/$name:$#> parsing arg '$1', next '$2'"
case ${1#--} in
# runtime & misc eapol_test options
-4|-6)
ip_version=${1#-};;
-o)
ecfg_extra+=("$2"); shift;;
-O)
save_chain=$2; shift;;
-x)
eapol_bin=$2; shift;;
help)
help "$2"; lib::exit;;
show-config)
dump_config=1;;
# RADIUS server
inherit)
if [[ $type == server ]]; then
die "cannot nest profile in server configuration"
fi
load_conf profile "$2"; shift;;
via)
load_conf server "$2"; shift;;
host)
[[ $type && $host ]] ||
host=$2; shift;;
port)
[[ $type && $secret ]] ||
port=$2; shift;;
proto)
[[ $type && $ip_version ]] ||
case $2 in
any) ip_version="";;
[46]) ip_version=$2;;
ip[46]) ip_version=${2#ip};;
ipv[46]) ip_version=${2#ipv};;
*) die "invalid IP protocol version '$2'";;
esac; shift;;
secret)
[[ $type && $secret ]] ||
secret=$2; shift;;
# mechanism choice
eap|inside|phase1)
eap=$2; shift;;
mech|using|phase2)
mech=$2; shift;;
# main (inner) identity
user|identity|login|inner-user|inner-identity|inner-login)
identity=$2; : ${password:="-"}; shift;;
pass)
password=$2; shift;;
cert|tls-cert)
tls_cert=$2; shift;;
key|tls-key)
tls_key=$2; shift;;
ca|tls-ca|ca-cert)
tls_ca=$2; shift;;
tls)
tls_opts+=("$2"); shift;;
# anonymous (outer) identity
anon)
outer_id="@";;
outer|outer-user|outer-identity|outer-login)
outer_id=$2; shift;;
# misc eapol options
subject-match)
ecfg_extra+=("subject_match=\"$2\""); shift;;
altsubject-match)
ecfg_extra+=("altsubject_match=\"$2\""); shift;;
domain-match|tls-hostname)
ecfg_extra+=("domain_match=\"$2\""); shift;;
domain-suffix-match|tls-domain)
ecfg_extra+=("domain_suffix_match=\"$2\""); shift;;
# etc.
*)
err "bad arg: \"$1\"";;
esac
fi
shift
done
}
parse_args "$@"
if [[ $profile && ! $host ]]; then
debug "server not set, trying to load '$profile' based on profile"
if [[ -v "server_$profile" ]]; then
load_conf server "$profile"
else
err "server for profile '$profile' not configured (use 'via …')"
fi
fi
# check for necessary tools
have 'radtest' ||
err "missing 'radtest' binary"
have "$eapol_bin" ||
err "missing '$eapol_bin' binary"
have 'name2addr' ||
have 'getent' ||
err "missing 'getent' or 'name2addr' binary"
(( !errors )) || exit
# check server parameters
if [[ ! $host && ! $profile ]]; then
die "missing profile name or RADIUS host address"
elif [[ ! $host ]]; then
die "missing RADIUS host address in profile"
elif [[ $host == *.*:* || $host == \[*\]:* ]]; then
die "invalid server name (use the 'port' option)"
elif [[ $host == *%* ]]; then
die "invalid server name (eapol_test does not support '%scope' yet)"
else
if have 'name2addr'; then
if [[ $ip_version ]]; then
arg="-$ip_version"
else
arg=""
fi
server_ip=$(name2addr $arg "$host")
elif have 'getent'; then
if [[ $ip_version ]]; then
arg="ahostsv$ip_version"
else
arg="ahosts"
fi
server_ip=$(getent $arg "$host" | awk '{print $1}' | head -1)
else
die "either 'name2addr' or 'getent' are required"
fi
if [[ $server_ip ]]; then
debug "resolved '$host' to '$server_ip'"
host=$server_ip
else
die "could not resolve RADIUS server '$host'"
fi
fi
[[ $port ]] || port=1812
[[ $secret ]] || err "missing RADIUS secret"
(( !errors )) || exit
# check client parameters
[[ $identity ]] || err "missing username"
[[ $outer_id ]] || outer_id=$identity
if [[ $outer_id == "@" ]]; then
outer_id+=${identity#*@}
fi
eap=${eap^^}
if [[ $eap == MSCHAP ]]; then
die "EAP-$eap does not exist; did you mean EAP-MSCHAPv2?"
fi
case $mech in
# only set a default for radtest, let eapol_test autoguess
'') mech='pap';;
# direct translation to eapol_test phase2
'='*) phase2=${mech#=};;
'pap') phase2='PAP';;
'chap') phase2='CHAP';;
'mschap') phase2='MSCHAP';;
'mschapv2') phase2='MSCHAPV2';;
'eap-gtc') phase2='EAP-GTC';;
'eap-md5') phase2='EAP-MD5';;
'eap-mschapv2') phase2='EAP-MSCHAPV2';;
# in case I ever try to make this nonsense work
'eap-pap') die "PAP is not an EAP mechanism; did you mean EAP-GTC?";;
'eap-chap') die "CHAP is not an EAP mechanism";;
'eap-mschap') die "EAP-MSCHAP does not exist; did you mean EAP-MSCHAPV2?";;
*) err "unknown mechanism '$mech'";;
esac
if [[ ${eap:-NONE} != @(NONE|TLS|PEAP|TTLS) && $phase2 ]]; then
die "EAP-$eap does not support inner mechanisms"
fi
if [[ $eap == PEAP && $phase2 && $phase2 != EAP-* ]]; then
case $phase2 in
PAP|GTC) notice "you probably want 'phase2 EAP-GTC'";;
MSCHAP*) notice "you probably want 'phase2 EAP-MSCHAPV2'";;
esac
die "EAP-$eap can only transport EAP mechanisms"
fi
if [[ ${eap:-NONE} == NONE && $mech != @(pap|chap|mschap|eap-md5) ]]; then
die "radtest does not support the '$mech' mechanism"
fi
if [[ ${eap:-NONE} != @(NONE|MD5|MSCHAPV2|OTP|GTC|TLS|PEAP|TTLS) ]]; then
die "eapol_test does not support the '$eap' EAP mechanism"
fi
if [[ $tls_ca == @(default|system) ]]; then
tls_ca=/etc/ssl/certs/ca-certificates.crt
[[ -f $tls_ca ]] ||
tls_ca=/etc/ssl/cert.pem
[[ -f $tls_ca ]] ||
die "could not find system TLS CA certificate bundle"
debug "found '$tls_ca'"
fi
if [[ $tls_ca && ! -f $tls_ca ]]; then
err "CA file '$tls_ca' does not exist"
fi
if [[ $eap == TLS ]]; then
unset password
if [[ $tls_key == same ]]; then
tls_key=$tls_cert
fi
if [[ ! $tls_cert ]]; then
err "missing certificate for EAP-$eap ('cert' option)"
elif [[ ! -f $tls_cert ]]; then
err "certificate '$tls_cert' does not exist"
fi
if [[ ! $tls_key ]]; then
err "missing private key for EAP-$eap ('key' option)"
elif [[ ! -f $tls_key ]]; then
err "key file '$tls_key' does not exist"
fi
else
if [[ $password == "-" ]]; then
read -s -p "password for '$identity': " password; echo
fi
if [[ ! $password ]]; then
err "missing password"
fi
fi
if [[ $eap == @(PEAP|TLS|TTLS) ]]; then
for opt in "${tls_opts[@]}"; do
case $opt in
# as of hostap_2_6-1911-ge8a7af9a3, v1.3 is enabled manually
'v1.3') phase1_opts+=("tls_disable_tlsv1_3=0");;
'no-v1.0') phase1_opts+=("tls_disable_tlsv1_0=1");;
'no-v1.1') phase1_opts+=("tls_disable_tlsv1_1=1");;
'no-v1.2') phase1_opts+=("tls_disable_tlsv1_2=1");;
'no-v1.3') phase1_opts+=("tls_disable_tlsv1_3=1");;
'tls_'*) phase1_opts+=("$opt");;
*) err "unknown TLS option '$opt'";;
esac
done
fi
(( ! errors )) || exit
# do the test
log "account: \"$identity\""
debug "password: \"$password\""
log "host: $host"
debug "secret: \"$secret\""
if [[ $eap && $eap != NONE ]]; then
# base options
ecfg_full=(
"eap=$eap"
)
if [[ ${phase1_opts[*]} ]]; then
ecfg_full+=("phase1=\"${phase1_opts[*]}\"")
fi
# phase 2
if [[ $eap == PEAP ]]; then
if [[ $phase2 == EAP-* ]]; then
ecfg_full+=("phase2=\"auth=${phase2#EAP-}\"")
elif [[ $phase2 ]]; then
err "EAP-$eap can only transport other EAP mechanisms"
fi
elif [[ $eap == TTLS ]]; then
if [[ $phase2 == EAP-* ]]; then
ecfg_full+=("phase2=\"autheap=${phase2#EAP-}\"")
elif [[ $phase2 ]]; then
ecfg_full+=("phase2=\"auth=${phase2}\"")
fi
fi
# credentials
ecfg_full+=("identity=\"$identity\"")
if [[ $password ]]; then
ecfg_full+=("password=\"$password\"")
fi
if [[ $outer_id ]]; then
ecfg_full+=("anonymous_identity=\"$outer_id\"")
fi
if [[ $tls_ca ]]; then
ecfg_full+=("ca_cert=\"$tls_ca\"")
fi
if [[ $tls_cert ]]; then
ecfg_full+=("client_cert=\"$tls_cert\"")
fi
if [[ $tls_key ]]; then
ecfg_full+=("private_key=\"$tls_key\"")
fi
# user-supplied settings
for opt in "${ecfg_extra[@]}"; do
case $opt in
# disallowed settings
'anonymous_identity='*)
err "'$opt': use 'outer-user ...' to set the anonymous identity";;
'eap='*)
err "'$opt': use 'eap ...' to set the outer EAP method";;
'identity='*)
err "'$opt': use 'user ...' to set the auth identity";;
'phase2='*)
err "'$opt': use 'phase2 ...' to set the inner EAP method";;
# magic values
'domain_match="*.'*'"')
debug "expanding '$opt' to domain_suffix_match"
opt="${opt#*=}"
opt="domain_suffix_match=${opt/'*.'/}"
debug "... converted to '$opt'"
;;&
'domain_suffix_match="auto"') ;&
'domain_suffix_match="@"')
debug "expanding '$opt'"
if [[ $identity == *@* ]]; then
opt="${opt%=*}=\"${identity#*@}\""
debug "... expanded to '$opt'"
else
err "cannot expand '$opt' from identity '$identity'"
fi
;;&
# rest
?*'='*)
ecfg_full+=("$opt");;
*)
err "supplicant option '$opt' given without value";;
esac
done
(( ! errors )) || exit
# if option given multiple times, the last value takes priority
for opt in "${ecfg_full[@]}"; do
k=${opt%%=*}
v=${opt#*=}
ecfg_values["$k"]=$v
done
log "mechanism: EAP-$eap (outer), ${phase2:-default} (inner)"
if [[ "$outer_id" != "$identity" ]]; then
log "anonymous identity: \"$outer_id\""
fi
# generate the eapol_test config
wd=$(mktemp -d /tmp/testrad.XXXXXXXX)
conf="$wd/eapol_test.conf"
{
echo "network={"
for opt in "${ecfg_full[@]}"; do
k=${opt%%=*}
v=${ecfg_values["$k"]}
if (( ecfg_seen["$k"]++ )); then
continue
fi
if [[ ! $v || $v == \"\" ]]; then
continue
fi
printf "\t%s=%s\n" "$k" "$v"
done
echo "}"
} > "$conf"
(( ! errors )) || exit
cmd=($eapol_bin -c "$conf" -a "$host" -p "$port" -s "$secret" -t 5
-M "22:44:66:42:42:42"
-C "testrad+eapol_test (${EMAIL:-$USERNAME@$HOSTNAME})")
if [[ $save_chain ]]; then
cmd+=(-o "$save_chain")
fi
if (( dump_config )); then
notice "dumping generated config and exiting"
cmd=(cat "$conf")
fi
else
log "mechanism: $phase2 (direct)"
if [[ "$outer_id" != "$identity" ]]; then
die "anonymous identity \"$outer_id\" not supported without EAP"
fi
if [[ $host == *:* ]]; then
host="[$host]"
fi
cmd=(radtest -t "$mech" "$identity" "$password" "$host:$port" 0 "$secret")
if (( dump_config )); then
notice "dumping generated config and exiting"
cmd=(: "${cmd[@]}")
fi
fi
echo "+ ${cmd[*]}"
if "${cmd[@]}"; then
r=0
log "test successful"
else
r=$?
err "test failed ($cmd returned $r)"
fi
if [[ -s $save_chain ]]; then
log "server certificate chain saved to '$save_chain'"
elif [[ $save_chain ]]; then
err "did not obtain server certificate chain"
fi
if [[ $wd == /tmp/testrad.* ]]; then
rm -rf "$wd"
fi
exit $r