#!/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. # # Parts 1b and 1c reuse the OpenSSH client on the echoserver: 1b forwards # ssh-agent through it, 1c checks that it forwards ports only with -F and # connects only to allowed destinations. # # 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 dest_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 if [ $dest_pid != $no_pid ] then kill -9 $dest_pid 2>/dev/null dest_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 1c: the echoserver forwards ports only when started with -F. # ExitOnForwardFailure turns a refused remote forward into exit status 255. # # $1 - the remote port to ask for, 0 to let the server pick # rest - extra echoserver arguments run_openssh_forward() { fwd_port=$1 shift case $echo_keys in ecc) start_echoserver -e "$@" ;; rsa) start_echoserver "$@" ;; *) start_echoserver -j "$work_dir/ed25519.pub" "$@" ;; esac rm -f "$client_out" "$client_log" printf '%s\n\003' "$probe" | \ ssh -T -p $port \ -o ExitOnForwardFailure=yes \ -R $fwd_port:127.0.0.1:9 \ -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 $! } if ./apps/wolfssh-options 2>/dev/null | grep -qx 'FWD'; then echo "Test the echoserver's forwarding opt-in" run_openssh_forward 0 [ $? -eq 255 ] && grep -q 'remote port forwarding failed' "$client_log" \ || fail "the echoserver forwarded a port without -F" run_openssh_forward 0 -F if [ $? -ne 0 ] || ! grep -q "$probe" "$client_out"; then cat "$client_log" fail "the echoserver refused a forward with -F" fi # A privileged port is refused by the echoserver's own check, which # logs it, not by a bind() failing for a non-root user. The server # exits after the session, flushing its log. run_openssh_forward 80 -F fwd_status=$? wait_with_limit $server_pid server_pid=$no_pid if [ $fwd_status -ne 255 ] || ! grep -q 'remote port forwarding failed' "$client_log" || ! grep -q 'Refusing to forward port 80\.' "$work_dir/server.log"; then cat "$client_log" "$work_dir/server.log" fail "the echoserver didn't refuse a forward of port 80" fi echo "the echoserver forwards only when asked to" # A direct-tcpip open goes only to loopback by default, and only to # the -L entries when there are any. Refused opens fail at once, so # 192.0.2.1 (TEST-NET-1) is never dialed. run_openssh_stdio() { stdio_dest=$1 shift case $echo_keys in ecc) start_echoserver -e "$@" ;; rsa) start_echoserver "$@" ;; *) start_echoserver -j "$work_dir/ed25519.pub" "$@" ;; esac rm -f "$client_out" "$client_log" ssh -T -W "$stdio_dest" -p $port \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o IdentitiesOnly=yes \ -o PreferredAuthentications=publickey \ -i "$work_dir/id" hansel@127.0.0.1 \ < /dev/null > "$client_out" 2> "$client_log" & wait_with_limit $! } run_openssh_stdio 192.0.2.1:9 -F [ $? -eq 255 ] && grep -q 'administratively prohibited' "$client_log" \ || fail "the echoserver forwarded to a non-loopback destination" run_openssh_stdio 127.0.0.1:9 -F -L 192.0.2.1:9 [ $? -eq 255 ] && grep -q 'administratively prohibited' "$client_log" \ || fail "the echoserver forwarded to a destination -L left out" # An allowed open reaches a second echoserver: an echo session with it # runs through a local forward over the first. $1 is the -L list, with # @PORT@ standing for the second echoserver's port. run_openssh_forward_allowed() { case $echo_keys in ecc) dest_keys=-e ;; rsa) dest_keys= ;; *) dest_keys="-j $work_dir/ed25519.pub" ;; esac # A port to forward from: one the OS just handed out and let go. start_echoserver local_port=$port kill -9 $server_pid 2>/dev/null wait $server_pid 2>/dev/null server_pid=$no_pid rm -f "$work_dir/dest_ready" ./examples/echoserver/echoserver -1 -f $dest_keys \ -R "$work_dir/dest_ready" > "$work_dir/dest.log" 2>&1 & dest_pid=$! counter=0 while [ ! -s "$work_dir/dest_ready" ] && [ "$counter" -lt 100 ]; do sleep 0.1 counter=$((counter + 1)) done [ ! -s "$work_dir/dest_ready" ] \ && fail "NO ready file, the destination echoserver never came up" dest_port=`cat "$work_dir/dest_ready"` allow_list=`echo "$1" | sed "s/@PORT@/$dest_port/g"` start_echoserver $dest_keys -F -L "$allow_list" # The outer session stays open while the inner one runs. rm -f "$work_dir/fwd_in" "$client_out" "$client_log" mkfifo "$work_dir/fwd_in" || fail "couldn't make a fifo" ssh -v -T -p $port \ -o ExitOnForwardFailure=yes \ -L $local_port:127.0.0.1:$dest_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" & outer_pid=$! exec 4> "$work_dir/fwd_in" counter=0 while ! grep -q 'Entering interactive session' "$client_log" && [ "$counter" -lt 100 ]; do sleep 0.1 counter=$((counter + 1)) done rm -f "$work_dir/inner_out" printf '%s\n\003' "$probe" | \ ssh -T -p $local_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/inner_out" 2> "$work_dir/inner_log" & wait_with_limit $! printf '\003' >&4 exec 4>&- wait_with_limit $outer_pid kill -9 $dest_pid 2>/dev/null wait $dest_pid 2>/dev/null dest_pid=$no_pid grep -q "$probe" "$work_dir/inner_out" } if ! run_openssh_forward_allowed 'example.invalid,127.0.0.1:@PORT@'; then cat "$client_log" "$work_dir/inner_log" fail "the echoserver refused a host:port destination -L named" fi if ! run_openssh_forward_allowed '127.0.0.1'; then cat "$client_log" "$work_dir/inner_log" fail "the echoserver refused a bare host -L named" fi echo "the echoserver forwards only to allowed destinations" 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