wolfssh/scripts/openssh-interop.test

659 lines
22 KiB
Bash
Executable File

#!/bin/sh
# OpenSSH interop test for strict key exchange, the Terrapin mitigation
# (CVE-2023-48795).
#
# Part 1 puts the OpenSSH client on the wolfSSH echoserver, part 2 the
# wolfSSH example client on OpenSSH's sshd. Each one checks that the marker
# is negotiated and that a session then runs over the connection: the
# mitigation zeroes the packet sequence numbers at SSH_MSG_NEWKEYS, and a
# peer that disagreed about the counters would fail the first packet that
# had to authenticate.
#
# Part 1b reuses the OpenSSH client on the echoserver to forward ssh-agent
# through it.
#
# Part 3 has the wolfSSH client authenticate to sshd with an RSA key held
# by OpenSSH's ssh-agent. The client has only the public half; the agent
# makes the signature.
#
# The checks read OpenSSH's own debug output, so they are worded the way
# OpenSSH words them. Strict KEX arrived in OpenSSH 9.6; anything older is
# skipped.
no_pid=-1
server_pid=$no_pid
sshd_pid=$no_pid
agent_pid=$no_pid
work_dir="`pwd`/openssh_interop_test$$"
ready_file="$work_dir/ready"
client_out="$work_dir/client.out"
client_log="$work_dir/client.log"
sshd_log="$work_dir/sshd.log"
port=0
counter=0
# What the echo session sends and expects back.
probe="strictkexprobe$$"
# Seconds any one connection is given before it is killed. Nothing here
# takes more than a moment; the limit only keeps a stuck session from
# hanging make check.
run_limit=60
killer_pid=-1
# The test keys are not copied into the build directory, so reach them
# through srcdir the way scripts/fwd.test does, or an out-of-tree build
# and make distcheck cannot find them. Made absolute because the example
# client chdirs up to the tree root before it opens the -i and -j files.
keys_dir=`cd "${srcdir:-.}/keys" && pwd` || exit 1
do_cleanup() {
if [ $killer_pid != $no_pid ]
then
kill $killer_pid 2>/dev/null
killer_pid=$no_pid
fi
if [ $sshd_pid != $no_pid ]
then
kill -9 $sshd_pid 2>/dev/null
sshd_pid=$no_pid
fi
if [ $server_pid != $no_pid ]
then
kill -9 $server_pid 2>/dev/null
server_pid=$no_pid
fi
if [ $agent_pid != $no_pid ]
then
kill $agent_pid 2>/dev/null
agent_pid=$no_pid
fi
rm -rf -- "$work_dir"
}
do_trap() {
echo "got trap"
do_cleanup
exit 1
}
trap do_trap INT TERM
# sshd holds a fixed port, so clean up on the way out however that happens.
trap do_cleanup EXIT
# Wait for a backgrounded command, killing it if it outstays the limit.
# timeout(1) is GNU coreutils and is on neither stock macOS nor the BSDs,
# so the watchdog is run here, the way scripts/sshclient.test does it.
# Polls rather than sleeping the whole limit: killing a subshell that is
# waiting on a sleep leaves the sleep running.
wait_with_limit() {
watched_pid=$1
(
watched=0
while kill -0 $watched_pid 2>/dev/null; do
if [ $watched -ge $run_limit ]; then
kill -9 $watched_pid 2>/dev/null
break
fi
sleep 1
watched=$((watched + 1))
done
) 2>/dev/null &
killer_pid=$!
wait $watched_pid
watched_status=$?
kill $killer_pid 2>/dev/null
killer_pid=$no_pid
return $watched_status
}
fail() {
printf '\n\n%s\n' "$1"
do_cleanup
exit 1
}
# Set WOLFSSH_TEST_REQUIRE_OPENSSH=1 where every part must run, as in CI;
# each skip below is then a failure.
require_openssh=${WOLFSSH_TEST_REQUIRE_OPENSSH:-0}
skip() {
if [ "$require_openssh" = "1" ]; then
fail "$1; WOLFSSH_TEST_REQUIRE_OPENSSH=1 does not allow skipping"
fi
printf '%s, skipping\n' "$1"
rm -rf -- "$work_dir"
exit 77
}
# Skip the part named by $2, keeping the parts that already passed.
skip_rest() {
if [ "$require_openssh" = "1" ]; then
fail "$1; WOLFSSH_TEST_REQUIRE_OPENSSH=1 does not allow skipping $2"
fi
echo "$1, skipping $2"
do_cleanup
exit 0
}
command -v ssh > /dev/null 2>&1 || skip "no ssh in the path"
ssh_version=`ssh -V 2>&1`
case "$ssh_version" in
OpenSSH_*) ;;
*) skip "ssh is not OpenSSH ($ssh_version)" ;;
esac
# 0 when the OpenSSH_X.Y in $1 is 9.6 or newer, 1 when it is older, and 2
# when there is no version to read. Strict KEX arrived in 9.6.
has_strict_kex() {
hsk_major=`echo "$1" | sed -n 's/.*OpenSSH_\([0-9][0-9]*\)\..*/\1/p'`
hsk_minor=`echo "$1" | \
sed -n 's/.*OpenSSH_[0-9][0-9]*\.\([0-9][0-9]*\).*/\1/p'`
[ -z "$hsk_major" ] && return 2
[ -z "$hsk_minor" ] && hsk_minor=0
if [ "$hsk_major" -lt 9 ] ||
{ [ "$hsk_major" -eq 9 ] && [ "$hsk_minor" -lt 6 ]; }
then
return 1
fi
return 0
}
has_strict_kex "$ssh_version"
case $? in
1) skip "$ssh_version predates strict KEX" ;;
2) skip "can't read the OpenSSH version ($ssh_version)" ;;
esac
[ ! -x ./examples/echoserver/echoserver ] && skip "echoserver doesn't exist"
./examples/echoserver/echoserver '-?' 2>&1 | grep -q "^echoserver " \
|| skip "echoserver doesn't run"
mkdir -p "$work_dir" || exit 1
# The echoserver is one shot, so start a new one per connection. It picks
# an ephemeral port and writes it to the ready file.
#
# -f keeps it in echo mode; without it a build with shell support tries to
# fork a login shell for a user who doesn't exist on this machine. Any
# argument given here is passed along, which is how the ECC sample key set
# is selected.
start_echoserver() {
if [ $server_pid != $no_pid ]
then
kill -9 $server_pid 2>/dev/null
wait $server_pid 2>/dev/null
server_pid=$no_pid
fi
rm -f "$ready_file"
./examples/echoserver/echoserver -1 -f "$@" -R "$ready_file" \
> "$work_dir/server.log" 2>&1 &
server_pid=$!
counter=0
while [ ! -s "$ready_file" ] && [ "$counter" -lt 100 ]; do
sleep 0.1
counter=$((counter + 1))
done
[ ! -s "$ready_file" ] && fail "NO ready file, the echoserver never came up"
port=`cat "$ready_file"`
}
# Run one echo session with the OpenSSH client.
#
# $1 - the private key to authenticate with, in a form OpenSSH reads
# rest - extra echoserver arguments
#
# The key is copied because OpenSSH refuses one that is readable by anyone
# else, and the checked-in keys are group and world readable. The 0x03 in
# the input is what tells the echoserver's worker the session is over; the
# newline before it is the line the server echoes back.
run_openssh_client() {
key_file=$1
shift
rm -f "$client_out" "$client_log"
cp "$key_file" "$work_dir/id" || fail "couldn't copy $key_file"
chmod 600 "$work_dir/id"
start_echoserver "$@"
printf '%s\n\003' "$probe" | \
ssh -vvv -T -p $port \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o IdentitiesOnly=yes \
-o PreferredAuthentications=publickey \
-i "$work_dir/id" hansel@127.0.0.1 \
> "$client_out" 2> "$client_log" &
wait_with_limit $!
}
echo "Test the OpenSSH client against the wolfSSH echoserver"
# -e switches the echoserver to its ECC sample keys. A build without ECDSA
# has only the RSA set, so fall back to that rather than call it a failure.
# A build with neither has only Ed25519, and the checked-in Ed25519 key is
# PKCS#8, which an OpenSSH on LibreSSL won't load, so make a key of
# OpenSSH's own and hand the echoserver its public half.
# Which of the attempts below opened the session, so the agent forwarding
# check can repeat it.
echo_keys=ecc
run_openssh_client "$keys_dir/hansel-key-ecc.pem" -e
session_status=$?
if [ $session_status -ne 0 ]; then
echo "the ECC attempt failed, retrying with the RSA sample key"
echo_keys=rsa
run_openssh_client "$keys_dir/hansel-key-rsa.pem"
session_status=$?
fi
if [ $session_status -ne 0 ] && command -v ssh-keygen > /dev/null 2>&1; then
echo "the RSA attempt failed, retrying with an Ed25519 key"
echo_keys=ed25519
ssh-keygen -q -t ed25519 -N '' -C hansel -f "$work_dir/ed25519" \
< /dev/null || fail "couldn't make an Ed25519 client key"
run_openssh_client "$work_dir/ed25519" -j "$work_dir/ed25519.pub"
session_status=$?
fi
if [ $session_status -ne 0 ]; then
cat "$client_log"
fail "couldn't open a session"
fi
# What the echoserver put in its KEXINIT. Both spellings go out; this is
# the one deployed OpenSSH matches on.
grep -q 'kex-strict-s-v00@openssh.com' "$client_log" \
|| fail "the echoserver didn't offer the strict KEX marker"
# And what OpenSSH decided to do about it.
grep -qi 'strict KEX ordering' "$client_log" \
|| fail "the OpenSSH client didn't turn strict KEX on"
grep -q "$probe" "$client_out" \
|| fail "the echoserver's reply didn't make it back"
# OpenSSH logs each reset as it happens. Informational: it is the one check
# here that a reworded debug line would break, and the round trip above
# already proves the two ends agree on the sequence numbers.
if grep -q 'resetting read seqnr' "$client_log"; then
echo "the OpenSSH client reset its inbound sequence number"
else
echo "note: no sequence number reset in the client log"
fi
echo "the OpenSSH client negotiated strict KEX with the echoserver"
# Part 1b forwards OpenSSH's ssh-agent through the echoserver. A local agent
# client on the echoserver's side lists the forwarded key over the socket
# the echoserver made for it, twice, so the second goes through a fresh
# accept. The socket's directory must be private and gone once the session
# is over.
if ! ./apps/wolfssh-options 2>/dev/null | grep -qx 'AGENT'; then
echo "the echoserver has no agent support, skipping agent forwarding"
elif ! command -v ssh-agent > /dev/null 2>&1 ||
! command -v ssh-add > /dev/null 2>&1 ||
! command -v ssh-keygen > /dev/null 2>&1 ||
! command -v mkfifo > /dev/null 2>&1; then
echo "no ssh-agent, ssh-add, ssh-keygen or mkfifo," \
"skipping agent forwarding"
else
echo "Test agent forwarding through the wolfSSH echoserver"
# The agent picks its own socket path; see Part 3.
SSH_AGENT_PID=
eval `ssh-agent -s` > /dev/null
[ -n "$SSH_AGENT_PID" ] || fail "couldn't start ssh-agent"
agent_pid=$SSH_AGENT_PID
ssh-keygen -q -t ed25519 -N '' -C wolfssh-agent-fwd \
-f "$work_dir/agent-fwd" < /dev/null \
|| fail "couldn't make a key for the agent"
ssh-add -q "$work_dir/agent-fwd" 2>/dev/null \
|| fail "couldn't add the key to ssh-agent"
# Long comments push the list reply past the echoserver's relay
# buffer, so it has to be drained in more than one read.
agent_pad=`printf '%1500s' '' | tr ' ' x`
for pad in 1 2 3 4; do
ssh-keygen -q -t ed25519 -N '' -C "wolfssh-agent-pad$pad-$agent_pad" \
-f "$work_dir/agent-pad$pad" < /dev/null \
|| fail "couldn't make a padding key for the agent"
ssh-add -q "$work_dir/agent-pad$pad" 2>/dev/null \
|| fail "couldn't add a padding key to ssh-agent"
done
# The client key was copied to $work_dir/id by the attempt that worked.
case $echo_keys in
ecc) start_echoserver -e ;;
rsa) start_echoserver ;;
*) start_echoserver -j "$work_dir/ed25519.pub" ;;
esac
# The session stays open while the agent is used through it.
rm -f "$work_dir/fwd_in" "$client_out" "$client_log"
mkfifo "$work_dir/fwd_in" || fail "couldn't make a fifo"
ssh -A -T -p $port \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o IdentitiesOnly=yes \
-o PreferredAuthentications=publickey \
-i "$work_dir/id" hansel@127.0.0.1 \
< "$work_dir/fwd_in" > "$client_out" 2> "$client_log" &
fwd_ssh_pid=$!
exec 3> "$work_dir/fwd_in"
printf '%s\n' "$probe" >&3
fwd_sock=
counter=0
while [ -z "$fwd_sock" ] && [ "$counter" -lt 100 ]; do
fwd_sock=`sed -n 's/^Agent socket: //p' "$work_dir/server.log"`
[ -z "$fwd_sock" ] && sleep 0.1
counter=$((counter + 1))
done
if [ -z "$fwd_sock" ]; then
exec 3>&-
cat "$work_dir/server.log"
fail "the echoserver never opened an agent socket"
fi
fwd_dir=`dirname "$fwd_sock"`
case `ls -ld "$fwd_dir"` in
drwx------*) ;;
*) exec 3>&-; fail "the agent socket's directory is not private" ;;
esac
for attempt in 1 2; do
SSH_AUTH_SOCK="$fwd_sock" ssh-add -l > "$work_dir/fwd_list" 2>&1 &
wait_with_limit $!
if ! grep -q 'wolfssh-agent-fwd' "$work_dir/fwd_list" ||
[ `grep -c 'wolfssh-agent-pad' "$work_dir/fwd_list"` -ne 4 ]; then
exec 3>&-
cat "$work_dir/fwd_list"
fail "the forwarded agent didn't list its keys (try $attempt)"
fi
done
printf '\003' >&3
exec 3>&-
wait_with_limit $fwd_ssh_pid
wait_with_limit $server_pid
server_pid=$no_pid
grep -q "$probe" "$client_out" \
|| fail "the session didn't survive the forwarded agent"
[ -e "$fwd_dir" ] && fail "the echoserver left $fwd_dir behind"
kill $agent_pid 2>/dev/null
agent_pid=$no_pid
echo "the forwarded agent answered through the echoserver"
fi
# Part 2 needs an sshd to talk to, and a host key to give it. Neither is
# worth failing the test over.
sshd_bin=
for candidate in /usr/sbin/sshd /usr/local/sbin/sshd /opt/homebrew/sbin/sshd
do
[ -x "$candidate" ] && sshd_bin="$candidate" && break
done
if [ -z "$sshd_bin" ]; then
sshd_bin=`command -v sshd 2>/dev/null`
fi
if [ -z "$sshd_bin" ] || [ ! -x "$sshd_bin" ]; then
skip_rest "no sshd on this machine" "the wolfSSH client half"
fi
if ! command -v ssh-keygen > /dev/null 2>&1; then
skip_rest "no ssh-keygen" "the wolfSSH client half"
fi
if [ ! -x ./examples/client/client ]; then
skip_rest "the example client wasn't built" "the wolfSSH client half"
fi
echo "Test the wolfSSH client against OpenSSH's sshd"
# RSA and Ed25519 host keys beside the ECDSA one, for a build without
# ECDSA. A build with neither ECDSA nor Ed25519 can verify only RSA.
ssh-keygen -q -t ecdsa -N '' -f "$work_dir/hostkey" < /dev/null \
|| fail "couldn't make a host key for sshd"
ssh-keygen -q -t rsa -b 3072 -N '' -f "$work_dir/hostkey-rsa" < /dev/null \
|| fail "couldn't make an RSA host key for sshd"
ssh-keygen -q -t ed25519 -N '' -f "$work_dir/hostkey-ed25519" < /dev/null \
|| fail "couldn't make an Ed25519 host key for sshd"
# sshd takes a fixed port, so try a few. -f /dev/null keeps the machine's
# own sshd_config out of it, and -ddd both raises the log level and keeps
# sshd in the foreground for one connection. ForceCommand stands in for the
# user's login shell: the client exits with the session's exit status, and
# whatever the user's dotfiles leave behind is not this test's business.
# Arguments after the port are passed to sshd.
start_sshd() {
sshd_listen_port=$1
shift
rm -f "$sshd_log"
"$sshd_bin" -ddd -f /dev/null -p "$sshd_listen_port" \
-h "$work_dir/hostkey" \
-h "$work_dir/hostkey-rsa" -h "$work_dir/hostkey-ed25519" \
-o "ListenAddress=127.0.0.1" \
-o "AuthorizedKeysFile=$work_dir/authorized_keys" \
-o "StrictModes=no" \
-o "PasswordAuthentication=no" \
-o "ForceCommand=true" \
-o "PidFile=none" "$@" > "$sshd_log" 2>&1 &
sshd_pid=$!
# Its own counter: the loop that calls this one is counting too. A
# failed bind is checked for as well as a good one -- sshd carries on
# after one address fails, so "Server listening" alone does not mean
# the port the client dials is answering.
sshd_wait=0
while [ "$sshd_wait" -lt 100 ]; do
grep -q 'failed: Address already in use' "$sshd_log" 2>/dev/null \
&& return 1
grep -q 'Server listening on' "$sshd_log" 2>/dev/null && return 0
kill -0 $sshd_pid 2>/dev/null || return 1
sleep 0.1
sshd_wait=$((sshd_wait + 1))
done
return 1
}
sshd_port=0
candidate_port=$((22500 + ($$ % 1000)))
counter=0
while [ "$counter" -lt 5 ]; do
if start_sshd $candidate_port; then
sshd_port=$candidate_port
break
fi
kill -9 $sshd_pid 2>/dev/null
sshd_pid=$no_pid
candidate_port=$((candidate_port + 1))
counter=$((counter + 1))
done
if [ "$sshd_port" -eq 0 ]; then
[ -s "$sshd_log" ] && tail -n 5 "$sshd_log"
skip_rest "sshd wouldn't start here" "the wolfSSH client half"
fi
# The version gate above covered the client. This sshd was found on its own
# and can be the older vendor one while a newer ssh sits earlier in PATH, so
# read what it says about itself before asking it for a marker it has never
# heard of.
sshd_version=`sed -n 's/.*sshd version \(OpenSSH_[^,]*\).*/\1/p' \
"$sshd_log" | head -n 1`
has_strict_kex "$sshd_version"
if [ $? -ne 0 ]; then
skip_rest \
"$sshd_bin is ${sshd_version:-an unreadable version}, no strict KEX" \
"the wolfSSH client half"
fi
# sshd authenticates the user running this test, with one of the canned
# client keys put in an authorized_keys file of its own.
login_user=${USER:-`id -un`}
[ -z "$login_user" ] && fail "couldn't work out who is running this"
run_wolfssh_client() {
cat "$2" > "$work_dir/authorized_keys" \
|| fail "couldn't write the authorized_keys file"
chmod 600 "$work_dir/authorized_keys"
# -x stops once the session is up: the KEX and the user auth are what
# this half is about, and the echo path was covered above. stdin is
# closed because the client prompts for a password when publickey is
# refused, and a prompt with nothing behind it hangs the run.
HOME="$work_dir" ./examples/client/client \
-h 127.0.0.1 -p $sshd_port -u "$login_user" -i "$1" -j "$2" -x \
< /dev/null > "$client_out" 2>&1 &
wait_with_limit $!
}
# sshd handles one connection per -d run, so it needs restarting between
# attempts. Wait for the old one to go first, or the restart races it for
# the port. Any arguments are passed to sshd.
restart_sshd() {
kill -9 $sshd_pid 2>/dev/null
wait $sshd_pid 2>/dev/null
sshd_pid=$no_pid
start_sshd $sshd_port "$@" || fail "couldn't restart sshd"
}
run_wolfssh_client "$keys_dir/hansel-key-ecc.der" \
"$keys_dir/hansel-key-ecc.pub"
if [ $? -ne 0 ]; then
echo "the ECC attempt failed, retrying with the RSA client key"
restart_sshd
run_wolfssh_client "$keys_dir/hansel-key-rsa.der" \
"$keys_dir/hansel-key-rsa.pub"
fi
if [ $? -ne 0 ]; then
echo "the RSA attempt failed, retrying with the Ed25519 client key"
restart_sshd
run_wolfssh_client "$keys_dir/hansel-key-ed25519.der" \
"$keys_dir/hansel-key-ed25519.pub"
fi
if [ $? -ne 0 ]; then
cat "$client_out"
tail -n 20 "$sshd_log"
fail "the wolfSSH client couldn't connect to sshd"
fi
# Give sshd a moment to finish writing its side of the session.
counter=0
while [ "$counter" -lt 50 ]; do
grep -q 'Accepted publickey' "$sshd_log" 2>/dev/null && break
sleep 0.1
counter=$((counter + 1))
done
grep -q 'kex-strict-c-v00@openssh.com' "$sshd_log" \
|| fail "the wolfSSH client didn't offer the strict KEX marker"
grep -qi 'strict KEX ordering' "$sshd_log" \
|| fail "sshd didn't turn strict KEX on"
grep -q 'Accepted publickey' "$sshd_log" \
|| fail "sshd never authenticated the wolfSSH client"
if grep -q 'resetting send seqnr' "$sshd_log"; then
echo "sshd reset its outbound sequence number"
else
echo "note: no sequence number reset in the sshd log"
fi
echo "the wolfSSH client negotiated strict KEX with sshd"
# Part 3 needs the client's agent support and OpenSSH's agent tools.
if ! ./examples/client/client '-?' 2>&1 | grep -q 'SSH-AGENT'; then
skip_rest "the example client has no agent support" "the agent half"
fi
if ! command -v ssh-agent > /dev/null 2>&1 ||
! command -v ssh-add > /dev/null 2>&1; then
skip_rest "no ssh-agent or ssh-add" "the agent half"
fi
# The RSA signature algorithms this client was built with; -E lists them
# without connecting.
agent_rsa_algs=
for alg in rsa-sha2-256 rsa-sha2-512; do
if ./examples/client/client -E -u x 2>&1 | grep -q ": $alg\$"; then
agent_rsa_algs="$agent_rsa_algs $alg"
fi
done
if [ -z "$agent_rsa_algs" ]; then
skip_rest "the client has no rsa-sha2 algorithms" "the agent half"
fi
echo "Test the wolfSSH client signing through ssh-agent for sshd"
# Run the client with only the public key $1, leaving the signature to the
# agent. $2 is the authorized_keys file sshd reads.
run_wolfssh_agent_client() {
cat "$2" > "$work_dir/authorized_keys" \
|| fail "couldn't write the authorized_keys file"
chmod 600 "$work_dir/authorized_keys"
rm -f "$client_out"
HOME="$work_dir" ./examples/client/client \
-h 127.0.0.1 -p $sshd_port -u "$login_user" -a -j "$1" -x \
< /dev/null > "$client_out" 2>&1 &
wait_with_limit $!
}
# Check that the client, which exited with status $1, succeeded and that
# sshd accepted the agent's signature under pkalg $2, an extended regular
# expression.
check_agent_auth() {
counter=0
while [ "$counter" -lt 50 ]; do
grep -q 'Accepted publickey' "$sshd_log" 2>/dev/null && break
sleep 0.1
counter=$((counter + 1))
done
if [ "$1" -ne 0 ] || ! grep -q 'Accepted publickey' "$sshd_log" ||
! grep -Eq "authenticated 1 pkalg ($2)( |\$)" "$sshd_log"; then
cat "$client_out"
tail -n 20 "$sshd_log"
fail "the client didn't authenticate with $2 signed by ssh-agent"
fi
}
# The agent picks its own socket path; one under work_dir can outgrow
# sun_path in a deep build directory.
SSH_AGENT_PID=
eval `ssh-agent -s` > /dev/null
[ -n "$SSH_AGENT_PID" ] || fail "couldn't start ssh-agent"
agent_pid=$SSH_AGENT_PID
# A key of ssh-keygen's default size, which the client doesn't have
# built in, so only the agent can sign for it.
ssh-keygen -q -t rsa -N '' -C wolfssh-agent -f "$work_dir/agent-rsa" \
< /dev/null || fail "couldn't make an RSA client key"
ssh-add -q "$work_dir/agent-rsa" 2>/dev/null \
|| fail "couldn't add the RSA key to ssh-agent"
for alg in $agent_rsa_algs; do
restart_sshd -o "PubkeyAcceptedAlgorithms=$alg"
run_wolfssh_agent_client "$work_dir/agent-rsa.pub" \
"$work_dir/agent-rsa.pub"
check_agent_auth $? "$alg"
done
echo "ssh-agent signed the wolfSSH client's RSA user auth"
do_cleanup
echo "OpenSSH interop tests passed"
exit 0