1#	$OpenBSD: agent-restrict.sh,v 1.6 2023/03/01 09:29:32 dtucker Exp $
2#	Placed in the Public Domain.
3
4tid="agent restrictions"
5
6SSH_AUTH_SOCK="$OBJ/agent.sock"
7export SSH_AUTH_SOCK
8rm -f $SSH_AUTH_SOCK $OBJ/agent.log $OBJ/host_[abcdex]* $OBJ/user_[abcdex]*
9rm -f $OBJ/sshd_proxy_host* $OBJ/ssh_output* $OBJ/expect_*
10rm -f $OBJ/ssh_proxy[._]* $OBJ/command
11
12verbose "generate keys"
13for h in a b c d e x ca ; do
14	$SSHKEYGEN -q -t ed25519 -C host_$h -N '' -f $OBJ/host_$h || \
15		fatal "ssh-keygen hostkey failed"
16	$SSHKEYGEN -q -t ed25519 -C user_$h -N '' -f $OBJ/user_$h || \
17		fatal "ssh-keygen userkey failed"
18done
19
20# Make some hostcerts
21for h in d e ; do
22	id="host_$h"
23	$SSHKEYGEN -q -s $OBJ/host_ca -I $id -n $id -h $OBJ/host_${h}.pub || \
24		fatal "ssh-keygen certify failed"
25done
26
27verbose "prepare client config"
28egrep -vi '(identityfile|hostname|hostkeyalias|proxycommand)' \
29	$OBJ/ssh_proxy > $OBJ/ssh_proxy.bak
30cat << _EOF > $OBJ/ssh_proxy
31IdentitiesOnly yes
32ForwardAgent yes
33ExitOnForwardFailure yes
34_EOF
35cp $OBJ/ssh_proxy $OBJ/ssh_proxy_noid
36for h in a b c d e ; do
37	cat << _EOF >> $OBJ/ssh_proxy
38Host host_$h
39	Hostname host_$h
40	HostkeyAlias host_$h
41	IdentityFile $OBJ/user_$h
42	ProxyCommand ${SUDO} env SSH_SK_HELPER=\"$SSH_SK_HELPER\" ${OBJ}/sshd-log-wrapper.sh -i -f $OBJ/sshd_proxy_host_$h
43_EOF
44	# Variant with no specified keys.
45	cat << _EOF >> $OBJ/ssh_proxy_noid
46Host host_$h
47	Hostname host_$h
48	HostkeyAlias host_$h
49	ProxyCommand ${SUDO} env SSH_SK_HELPER=\"$SSH_SK_HELPER\" ${OBJ}/sshd-log-wrapper.sh -i -f $OBJ/sshd_proxy_host_$h
50_EOF
51done
52cat $OBJ/ssh_proxy.bak >> $OBJ/ssh_proxy
53cat $OBJ/ssh_proxy.bak >> $OBJ/ssh_proxy_noid
54
55LC_ALL=C
56export LC_ALL
57echo "SetEnv LC_ALL=${LC_ALL}" >> sshd_proxy
58
59verbose "prepare known_hosts"
60rm -f $OBJ/known_hosts
61for h in a b c x ; do
62	(printf "host_$h " ; cat $OBJ/host_${h}.pub) >> $OBJ/known_hosts
63done
64(printf "@cert-authority host_* " ; cat $OBJ/host_ca.pub) >> $OBJ/known_hosts
65
66verbose "prepare server configs"
67egrep -vi '(hostkey|pidfile)' $OBJ/sshd_proxy \
68	> $OBJ/sshd_proxy.bak
69for h in a b c d e; do
70	cp $OBJ/sshd_proxy.bak $OBJ/sshd_proxy_host_$h
71	cat << _EOF >> $OBJ/sshd_proxy_host_$h
72ExposeAuthInfo yes
73PidFile none
74Hostkey $OBJ/host_$h
75_EOF
76done
77for h in d e ; do
78	echo "HostCertificate $OBJ/host_${h}-cert.pub" \
79		>> $OBJ/sshd_proxy_host_$h
80done
81# Create authorized_keys with canned command.
82reset_keys() {
83	_whichcmd="$1"
84	_command=""
85	case "$_whichcmd" in
86	authinfo)	_command="cat \$SSH_USER_AUTH" ;;
87	keylist)		_command="$SSHADD -L | cut -d' ' -f-2 | sort" ;;
88	*)		fatal "unsupported command $_whichcmd" ;;
89	esac
90	trace "reset keys"
91	>$OBJ/authorized_keys_$USER
92	for h in e d c b a; do
93		(printf "%s" "restrict,agent-forwarding,command=\"$_command\" ";
94		 cat $OBJ/user_$h.pub) >> $OBJ/authorized_keys_$USER
95	done
96}
97# Prepare a key for comparison with ExposeAuthInfo/$SSH_USER_AUTH.
98expect_key() {
99	_key="$OBJ/${1}.pub"
100	_file="$OBJ/$2"
101	(printf "publickey " ; cut -d' ' -f-2 $_key) > $_file
102}
103# Prepare expect_* files to compare against authinfo forced command to ensure
104# keys used for authentication match.
105reset_expect_keys() {
106	for u in a b c d e; do
107		expect_key user_$u expect_$u
108	done
109}
110# ssh to host, expecting success and that output matched expectation for
111# that host (expect_$h file).
112expect_succeed() {
113	_id="$1"
114	_case="$2"
115	shift; shift; _extra="$@"
116	_host="host_$_id"
117	trace "connect $_host expect success"
118	rm -f $OBJ/ssh_output
119	${SSH} $_extra -F $OBJ/ssh_proxy $_host true > $OBJ/ssh_output
120	_s=$?
121	test $_s -eq 0 || fail "host $_host $_case fail, exit status $_s"
122	diff $OBJ/ssh_output $OBJ/expect_${_id} ||
123		fail "unexpected ssh output"
124}
125# ssh to host using explicit key, expecting success and that the key was
126# actually used for authentication.
127expect_succeed_key() {
128	_id="$1"
129	_key="$2"
130	_case="$3"
131	shift; shift; shift; _extra="$@"
132	_host="host_$_id"
133	trace "connect $_host expect success, with key $_key"
134	_keyfile="$OBJ/$_key"
135	rm -f $OBJ/ssh_output
136	${SSH} $_extra -F $OBJ/ssh_proxy_noid \
137	    -oIdentityFile=$_keyfile $_host true > $OBJ/ssh_output
138	_s=$?
139	test $_s -eq 0 || fail "host $_host $_key $_case fail, exit status $_s"
140	expect_key $_key expect_key
141	diff $OBJ/ssh_output $OBJ/expect_key ||
142		fail "incorrect key used for authentication"
143}
144# ssh to a host, expecting it to fail.
145expect_fail() {
146	_host="$1"
147	_case="$2"
148	shift; shift; _extra="$@"
149	trace "connect $_host expect failure"
150	${SSH} $_extra -F $OBJ/ssh_proxy $_host true >/dev/null && \
151		fail "host $_host $_case succeeded unexpectedly"
152}
153# ssh to a host using an explicit key, expecting it to fail.
154expect_fail_key() {
155	_id="$1"
156	_key="$2"
157	_case="$3"
158	shift; shift; shift; _extra="$@"
159	_host="host_$_id"
160	trace "connect $_host expect failure, with key $_key"
161	_keyfile="$OBJ/$_key"
162	${SSH} $_extra -F $OBJ/ssh_proxy_noid -oIdentityFile=$_keyfile \
163	    $_host true > $OBJ/ssh_output && \
164		fail "host $_host $_key $_case succeeded unexpectedly"
165}
166# Move the private key files out of the way to force use of agent-hosted keys.
167hide_privatekeys() {
168	trace "hide private keys"
169	for u in a b c d e x; do
170		mv $OBJ/user_$u $OBJ/user_x$u || fatal "hide privkey $u"
171	done
172}
173# Put the private key files back.
174restore_privatekeys() {
175	trace "restore private keys"
176	for u in a b c d e x; do
177		mv $OBJ/user_x$u $OBJ/user_$u || fatal "restore privkey $u"
178	done
179}
180clear_agent() {
181	${SSHADD} -D > /dev/null 2>&1 || fatal "clear agent failed"
182}
183
184reset_keys authinfo
185reset_expect_keys
186
187verbose "authentication w/o agent"
188for h in a b c d e ; do
189	expect_succeed $h "w/o agent"
190	wrongkey=user_e
191	test "$h" = "e" && wrongkey=user_a
192	expect_succeed_key $h $wrongkey "\"wrong\" key w/o agent"
193done
194hide_privatekeys
195for h in a b c d e ; do
196	expect_fail $h "w/o agent"
197done
198restore_privatekeys
199
200verbose "start agent"
201${SSHAGENT} ${EXTRA_AGENT_ARGS} -d -a $SSH_AUTH_SOCK > $OBJ/agent.log 2>&1 &
202AGENT_PID=$!
203trap "kill $AGENT_PID" EXIT
204sleep 4 # Give it a chance to start
205# Check that it's running.
206${SSHADD} -l > /dev/null 2>&1
207if [ $? -ne 1 ]; then
208	fail "ssh-add -l did not fail with exit code 1"
209fi
210
211verbose "authentication with agent (no restrict)"
212for u in a b c d e x; do
213	$SSHADD -q $OBJ/user_$u || fatal "add key $u unrestricted"
214done
215hide_privatekeys
216for h in a b c d e ; do
217	expect_succeed $h "with agent"
218	wrongkey=user_e
219	test "$h" = "e" && wrongkey=user_a
220	expect_succeed_key $h $wrongkey "\"wrong\" key with agent"
221done
222
223verbose "unrestricted keylist"
224reset_keys keylist
225rm -f $OBJ/expect_list.pre
226# List of keys from agent should contain everything.
227for u in a b c d e x; do
228	cut -d " " -f-2 $OBJ/user_${u}.pub >> $OBJ/expect_list.pre
229done
230sort $OBJ/expect_list.pre > $OBJ/expect_list
231for h in a b c d e; do
232	cp $OBJ/expect_list $OBJ/expect_$h
233	expect_succeed $h "unrestricted keylist"
234done
235restore_privatekeys
236
237verbose "authentication with agent (basic restrict)"
238reset_keys authinfo
239reset_expect_keys
240for h in a b c d e; do
241	$SSHADD -h host_$h -H $OBJ/known_hosts -q $OBJ/user_$h \
242		|| fatal "add key $u basic restrict"
243done
244# One more, unrestricted
245$SSHADD -q $OBJ/user_x || fatal "add unrestricted key"
246hide_privatekeys
247# Authentication to host with expected key should work.
248for h in a b c d e ; do
249	expect_succeed $h "with agent"
250done
251# Authentication to host with incorrect key should fail.
252verbose "authentication with agent incorrect key (basic restrict)"
253for h in a b c d e ; do
254	wrongkey=user_e
255	test "$h" = "e" && wrongkey=user_a
256	expect_fail_key $h $wrongkey "wrong key with agent (basic restrict)"
257done
258
259verbose "keylist (basic restrict)"
260reset_keys keylist
261# List from forwarded agent should contain only user_x - the unrestricted key.
262cut -d " " -f-2 $OBJ/user_x.pub > $OBJ/expect_list
263for h in a b c d e; do
264	cp $OBJ/expect_list $OBJ/expect_$h
265	expect_succeed $h "keylist (basic restrict)"
266done
267restore_privatekeys
268
269verbose "username"
270reset_keys authinfo
271reset_expect_keys
272for h in a b c d e; do
273	$SSHADD -h "${USER}@host_$h" -H $OBJ/known_hosts -q $OBJ/user_$h \
274		|| fatal "add key $u basic restrict"
275done
276hide_privatekeys
277for h in a b c d e ; do
278	expect_succeed $h "wildcard user"
279done
280restore_privatekeys
281
282verbose "username wildcard"
283reset_keys authinfo
284reset_expect_keys
285for h in a b c d e; do
286	$SSHADD -h "*@host_$h" -H $OBJ/known_hosts -q $OBJ/user_$h \
287		|| fatal "add key $u basic restrict"
288done
289hide_privatekeys
290for h in a b c d e ; do
291	expect_succeed $h "wildcard user"
292done
293restore_privatekeys
294
295verbose "username incorrect"
296reset_keys authinfo
297reset_expect_keys
298for h in a b c d e; do
299	$SSHADD -h "--BADUSER@host_$h" -H $OBJ/known_hosts -q $OBJ/user_$h \
300		|| fatal "add key $u basic restrict"
301done
302hide_privatekeys
303for h in a b c d e ; do
304	expect_fail $h "incorrect user"
305done
306restore_privatekeys
307
308
309verbose "agent restriction honours certificate principal"
310reset_keys authinfo
311reset_expect_keys
312clear_agent
313$SSHADD -h host_e -H $OBJ/known_hosts -q $OBJ/user_d || fatal "add key"
314hide_privatekeys
315expect_fail d "restricted agent w/ incorrect cert principal"
316restore_privatekeys
317
318# Prepares the script used to drive chained ssh connections for the
319# multihop tests. Believe me, this is easier than getting the escaping
320# right for 5 hops on the command-line...
321prepare_multihop_script() {
322	MULTIHOP_RUN=$OBJ/command
323	cat << _EOF > $MULTIHOP_RUN
324#!/bin/sh
325#set -x
326me="\$1" ; shift
327next="\$1"
328if test ! -z "\$me" ; then 
329	rm -f $OBJ/done
330	echo "HOSTNAME host_\$me"
331	echo "AUTHINFO"
332	cat \$SSH_USER_AUTH
333fi
334echo AGENT
335$SSHADD -L | egrep "^ssh" | cut -d" " -f-2 | sort
336if test -z "\$next" ; then 
337	touch $OBJ/done
338	echo "FINISH"
339	e=0
340else
341	echo NEXT
342	${SSH} -F $OBJ/ssh_proxy_noid -oIdentityFile=$OBJ/user_a \
343		host_\$next $MULTIHOP_RUN "\$@"
344	e=\$?
345fi
346echo "COMPLETE \"\$me\""
347if test ! -z "\$me" ; then 
348	if test ! -f $OBJ/done ; then
349		echo "DONE MARKER MISSING"
350		test \$e -eq 0 && e=63
351	fi
352fi
353exit \$e
354_EOF
355	chmod u+x $MULTIHOP_RUN
356}
357
358# Prepare expected output for multihop tests at expect_a
359prepare_multihop_expected() {
360	_keys="$1"
361	_hops="a b c d e"
362	test -z "$2" || _hops="$2"
363	_revhops=$(echo "$_hops" | rev)
364	_lasthop=$(echo "$_hops" | sed 's/.* //')
365
366	rm -f $OBJ/expect_keys
367	for h in a b c d e; do
368		cut -d" " -f-2 $OBJ/user_${h}.pub >> $OBJ/expect_keys
369	done
370	rm -f $OBJ/expect_a
371	echo "AGENT" >> $OBJ/expect_a
372	test "x$_keys" = "xnone" || sort $OBJ/expect_keys >> $OBJ/expect_a
373	echo "NEXT" >> $OBJ/expect_a
374	for h in $_hops ; do 
375		echo "HOSTNAME host_$h" >> $OBJ/expect_a
376		echo "AUTHINFO" >> $OBJ/expect_a
377		(printf "publickey " ; cut -d" " -f-2 $OBJ/user_a.pub) >> $OBJ/expect_a
378		echo "AGENT" >> $OBJ/expect_a
379		if test "x$_keys" = "xall" ; then
380			sort $OBJ/expect_keys >> $OBJ/expect_a
381		fi
382		if test "x$h" != "x$_lasthop" ; then
383			if test "x$_keys" = "xfiltered" ; then
384				cut -d" " -f-2 $OBJ/user_a.pub >> $OBJ/expect_a
385			fi
386			echo "NEXT" >> $OBJ/expect_a
387		fi
388	done
389	echo "FINISH" >> $OBJ/expect_a
390	for h in $_revhops "" ; do 
391		echo "COMPLETE \"$h\"" >> $OBJ/expect_a
392	done
393}
394
395prepare_multihop_script
396cp $OBJ/user_a.pub $OBJ/authorized_keys_$USER # only one key used.
397
398verbose "multihop without agent"
399clear_agent
400prepare_multihop_expected none
401$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output || fail "multihop no agent ssh failed"
402diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output"
403
404verbose "multihop agent unrestricted"
405clear_agent
406$SSHADD -q $OBJ/user_[abcde]
407prepare_multihop_expected all
408$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output || fail "multihop no agent ssh failed"
409diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output"
410
411verbose "multihop restricted"
412clear_agent
413prepare_multihop_expected filtered
414# Add user_a, with permission to connect through the whole chain.
415$SSHADD -h host_a -h "host_a>host_b" -h "host_b>host_c" \
416	-h "host_c>host_d" -h "host_d>host_e" \
417	-H $OBJ/known_hosts -q $OBJ/user_a \
418	|| fatal "add key user_a multihop"
419# Add the other keys, bound to a unused host.
420$SSHADD -q -h host_x -H $OBJ/known_hosts $OBJ/user_[bcde] || fail "add keys"
421hide_privatekeys
422$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output || fail "multihop ssh failed"
423diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output"
424restore_privatekeys
425
426verbose "multihop username"
427$SSHADD -h host_a -h "host_a>${USER}@host_b" -h "host_b>${USER}@host_c" \
428	-h "host_c>${USER}@host_d"  -h "host_d>${USER}@host_e" \
429	-H $OBJ/known_hosts -q $OBJ/user_a || fatal "add key user_a multihop"
430hide_privatekeys
431$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output || fail "multihop w/ user ssh failed"
432diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output"
433restore_privatekeys
434
435verbose "multihop wildcard username"
436$SSHADD -h host_a -h "host_a>*@host_b" -h "host_b>*@host_c" \
437	-h "host_c>*@host_d"  -h "host_d>*@host_e" \
438	-H $OBJ/known_hosts -q $OBJ/user_a || fatal "add key user_a multihop"
439hide_privatekeys
440$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output || fail "multihop w/ user ssh failed"
441diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output"
442restore_privatekeys
443
444verbose "multihop wrong username"
445$SSHADD -h host_a -h "host_a>*@host_b" -h "host_b>*@host_c" \
446	-h "host_c>--BADUSER@host_d"  -h "host_d>*@host_e" \
447	-H $OBJ/known_hosts -q $OBJ/user_a || fatal "add key user_a multihop"
448hide_privatekeys
449$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output && \
450	fail "multihop with wrong user succeeded unexpectedly"
451restore_privatekeys
452
453verbose "multihop cycle no agent"
454clear_agent
455prepare_multihop_expected none "a b a a c d e"
456$MULTIHOP_RUN "" a b a a c d e > $OBJ/ssh_output || \
457	fail "multihop cycle no-agent fail"
458diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output"
459
460verbose "multihop cycle agent unrestricted"
461clear_agent
462$SSHADD -q $OBJ/user_[abcde] || fail "add keys"
463prepare_multihop_expected all "a b a a c d e"
464$MULTIHOP_RUN "" a b a a c d e > $OBJ/ssh_output || \
465	fail "multihop cycle agent ssh failed"
466diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output"
467
468verbose "multihop cycle restricted deny"
469clear_agent
470$SSHADD -q -h host_x -H $OBJ/known_hosts $OBJ/user_[bcde] || fail "add keys"
471$SSHADD -h host_a -h "host_a>host_b" -h "host_b>host_c" \
472	-h "host_c>host_d" -h "host_d>host_e" \
473	-H $OBJ/known_hosts -q $OBJ/user_a \
474	|| fatal "add key user_a multihop"
475prepare_multihop_expected filtered "a b a a c d e"
476hide_privatekeys
477$MULTIHOP_RUN "" a b a a c d e > $OBJ/ssh_output && \
478	fail "multihop cycle restricted deny succeded unexpectedly"
479restore_privatekeys
480
481verbose "multihop cycle restricted allow"
482clear_agent
483$SSHADD -q -h host_x -H $OBJ/known_hosts $OBJ/user_[bcde] || fail "add keys"
484$SSHADD -h host_a -h "host_a>host_b" -h "host_b>host_c" \
485	-h "host_c>host_d" -h "host_d>host_e" \
486	-h "host_b>host_a" -h "host_a>host_a" -h "host_a>host_c" \
487	-H $OBJ/known_hosts -q $OBJ/user_a \
488	|| fatal "add key user_a multihop"
489prepare_multihop_expected filtered "a b a a c d e"
490hide_privatekeys
491$MULTIHOP_RUN "" a b a a c d e > $OBJ/ssh_output || \
492	fail "multihop cycle restricted allow failed"
493diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output"
494restore_privatekeys
495
496