1#!/bin/sh
2
3#	$NetBSD: t_certctl.sh,v 1.10 2023/09/05 12:32:30 riastradh Exp $
4#
5# Copyright (c) 2023 The NetBSD Foundation, Inc.
6# All rights reserved.
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions
10# are met:
11# 1. Redistributions of source code must retain the above copyright
12#    notice, this list of conditions and the following disclaimer.
13# 2. Redistributions in binary form must reproduce the above copyright
14#    notice, this list of conditions and the following disclaimer in the
15#    documentation and/or other materials provided with the distribution.
16#
17# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
18# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
19# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
21# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27# POSSIBILITY OF SUCH DAMAGE.
28#
29
30CERTCTL="certctl -C certs.conf -c certs -u untrusted"
31
32# setupconf <subdir>...
33#
34#	Create certs/ and set up certs.conf to search the specified
35#	subdirectories of the source directory.
36#
37setupconf()
38{
39	local sep subdir dir
40
41	mkdir certs
42	cat <<EOF >certs.conf
43netbsd-certctl 20230816
44
45# comment at line start
46	# comment not at line start, plus some intentional whitespace
47   
48# THE WHITESPACE ABOVE IS INTENTIONAL, DO NOT DELETE
49EOF
50	# Start with a continuation line separator; then switch to
51	# non-continuation lines.
52	sep=$(printf ' \\\n\t')
53	for subdir; do
54		dir=$(atf_get_srcdir)/$subdir
55		cat <<EOF >>certs.conf
56path$sep$(printf '%s' "$dir" | vis -M)
57EOF
58		sep=' '
59	done
60}
61
62# check_empty
63#
64#	Verify the certs directory is empty after dry runs or after
65#	clearing the directory.
66#
67check_empty()
68{
69	local why
70
71	why=${1:-dry run}
72	for x in certs/*; do
73		if [ -e "$x" -o -h "$x" ]; then
74			atf_fail "certs/ should be empty after $why"
75		fi
76	done
77}
78
79# check_nonempty
80#
81#	Verify the certs directory is nonempty.
82#
83check_nonempty()
84{
85	for x in certs/*.0; do
86		test -e "$x" && test -h "$x" && return
87	done
88	atf_fail "certs/ should be nonempty"
89}
90
91# checks <certsN>...
92#
93#	Run various checks with certctl.
94#
95checks()
96{
97	local certs1 diginotar_base diginotar diginotar_hash subdir srcdir
98
99	certs1=$(atf_get_srcdir)/certs1
100	diginotar_base=Explicitly_Distrust_DigiNotar_Root_CA.pem
101	diginotar=$certs1/$diginotar_base
102	diginotar_hash=$(openssl x509 -hash -noout <$diginotar)
103
104	# Do a dry run of rehash and make sure the directory is still
105	# empty.
106	atf_check -s exit:0 $CERTCTL -n rehash
107	check_empty
108
109	# Distrust and trust one CA, as a dry run.  The trust should
110	# fail because it's not currently distrusted.
111	atf_check -s exit:0 $CERTCTL -n untrust "$diginotar"
112	check_empty
113	atf_check -s not-exit:0 -e match:currently \
114	    $CERTCTL -n trust "$diginotar"
115	check_empty
116
117	# Do a real rehash, not a dry run.
118	atf_check -s exit:0 $CERTCTL rehash
119
120	# Make sure all the certificates are trusted.
121	for subdir; do
122		case $subdir in
123		/*)	srcdir=$subdir;;
124		*)	srcdir=$(atf_get_srcdir)/$subdir;;
125		esac
126		for cert in "$srcdir"/*.pem; do
127			# Verify the certificate is linked by its base name.
128			certbase=$(basename "$cert")
129			atf_check -s exit:0 -o inline:"$cert" \
130			    readlink -n "certs/$certbase"
131
132			# Verify the certificate is linked by a hash.
133			hash=$(openssl x509 -hash -noout <$cert)
134			counter=0
135			found=false
136			while [ $counter -lt 10 ]; do
137				if cmp -s "certs/$hash.$counter" "$cert"; then
138					found=true
139					break
140				fi
141				counter=$((counter + 1))
142			done
143			if ! $found; then
144				atf_fail "missing $cert"
145			fi
146
147			# Delete both links.
148			rm "certs/$certbase"
149			rm "certs/$hash.$counter"
150		done
151	done
152
153	# Verify the certificate bundle is there with the right
154	# permissions (0644) and delete it.
155	#
156	# XXX Verify its content.
157	atf_check -s exit:0 test -f certs/ca-certificates.crt
158	atf_check -s exit:0 test ! -h certs/ca-certificates.crt
159	atf_check -s exit:0 -o inline:'100644\n' \
160	    stat -f %p certs/ca-certificates.crt
161	rm certs/ca-certificates.crt
162
163	# Make sure after deleting everything there's nothing left.
164	check_empty "removing all expected certificates"
165
166	# Distrust, trust, and re-distrust one CA, and verify that it
167	# ceases to appear, reappears, and again ceases to appear.
168	# (This one has no subject hash collisions to worry about, so
169	# we hard-code the `.0' suffix.)
170	atf_check -s exit:0 $CERTCTL untrust "$diginotar"
171	atf_check -s exit:0 test -e "untrusted/$diginotar_base"
172	atf_check -s exit:0 test -h "untrusted/$diginotar_base"
173	atf_check -s exit:0 test ! -e "certs/$diginotar_base"
174	atf_check -s exit:0 test ! -h "certs/$diginotar_base"
175	atf_check -s exit:0 test ! -e "certs/$diginotar_hash.0"
176	atf_check -s exit:0 test ! -h "certs/$diginotar_hash.0"
177	check_nonempty
178
179	atf_check -s exit:0 $CERTCTL trust "$diginotar"
180	atf_check -s exit:0 test ! -e "untrusted/$diginotar_base"
181	atf_check -s exit:0 test ! -h "untrusted/$diginotar_base"
182	atf_check -s exit:0 test -e "certs/$diginotar_base"
183	atf_check -s exit:0 test -h "certs/$diginotar_base"
184	atf_check -s exit:0 test -e "certs/$diginotar_hash.0"
185	atf_check -s exit:0 test -h "certs/$diginotar_hash.0"
186	rm "certs/$diginotar_base"
187	rm "certs/$diginotar_hash.0"
188	check_nonempty
189
190	atf_check -s exit:0 $CERTCTL untrust "$diginotar"
191	atf_check -s exit:0 test -e "untrusted/$diginotar_base"
192	atf_check -s exit:0 test -h "untrusted/$diginotar_base"
193	atf_check -s exit:0 test ! -e "certs/$diginotar_base"
194	atf_check -s exit:0 test ! -h "certs/$diginotar_base"
195	atf_check -s exit:0 test ! -e "certs/$diginotar_hash.0"
196	atf_check -s exit:0 test ! -h "certs/$diginotar_hash.0"
197	check_nonempty
198}
199
200atf_test_case empty
201empty_head()
202{
203	atf_set "descr" "Test empty certificates store"
204}
205empty_body()
206{
207	setupconf		# no directories
208	check_empty "empty cert path"
209	atf_check -s exit:0 $CERTCTL -n rehash
210	check_empty
211	atf_check -s exit:0 $CERTCTL rehash
212	atf_check -s exit:0 test -f certs/ca-certificates.crt
213	atf_check -s exit:0 test \! -h certs/ca-certificates.crt
214	atf_check -s exit:0 test \! -s certs/ca-certificates.crt
215	atf_check -s exit:0 rm certs/ca-certificates.crt
216	check_empty "empty cert path"
217}
218
219atf_test_case onedir
220onedir_head()
221{
222	atf_set "descr" "Test one certificates directory"
223}
224onedir_body()
225{
226	setupconf certs1
227	checks certs1
228}
229
230atf_test_case twodir
231twodir_head()
232{
233	atf_set "descr" "Test two certificates directories"
234}
235twodir_body()
236{
237	setupconf certs1 certs2
238	checks certs1 certs2
239}
240
241atf_test_case collidehash
242collidehash_head()
243{
244	atf_set "descr" "Test colliding hashes"
245}
246collidehash_body()
247{
248	# certs3 has two certificates with the same subject hash
249	setupconf certs1 certs3
250	checks certs1 certs3
251}
252
253atf_test_case collidebase
254collidebase_head()
255{
256	atf_set "descr" "Test colliding base names"
257}
258collidebase_body()
259{
260	# certs1 and certs4 both have DigiCert_Global_Root_CA.pem,
261	# which should cause list and rehash to fail and mention
262	# duplicates.
263	setupconf certs1 certs4
264	atf_check -s not-exit:0 -o ignore -e match:duplicate $CERTCTL list
265	atf_check -s not-exit:0 -o ignore -e match:duplicate $CERTCTL rehash
266}
267
268atf_test_case manual
269manual_head()
270{
271	atf_set "descr" "Test manual operation"
272}
273manual_body()
274{
275	local certs1 diginotar_base diginotar diginotar_hash
276
277	certs1=$(atf_get_srcdir)/certs1
278	diginotar_base=Explicitly_Distrust_DigiNotar_Root_CA.pem
279	diginotar=$certs1/$diginotar_base
280	diginotar_hash=$(openssl x509 -hash -noout <$diginotar)
281
282	setupconf certs1 certs2
283	cat <<EOF >>certs.conf
284manual
285EOF
286	touch certs/bogus.pem
287	ln -s bogus.pem certs/0123abcd.0
288
289	# Listing shouldn't mention anything in the certs/ cache.
290	atf_check -s exit:0 -o not-match:bogus $CERTCTL list
291	atf_check -s exit:0 -o not-match:bogus $CERTCTL untrusted
292
293	# Rehashing and changing the configuration should succeed, but
294	# mention `manual' in a warning message and should not touch
295	# the cache.
296	atf_check -s exit:0 -e match:manual $CERTCTL rehash
297	atf_check -s exit:0 -e match:manual $CERTCTL untrust "$diginotar"
298	atf_check -s exit:0 -e match:manual $CERTCTL trust "$diginotar"
299
300	# The files we created should still be there.
301	atf_check -s exit:0 test -f certs/bogus.pem
302	atf_check -s exit:0 test -h certs/0123abcd.0
303}
304
305atf_test_case evilcertsdir
306evilcertsdir_head()
307{
308	atf_set "descr" "Test certificate directory with evil characters"
309}
310evilcertsdir_body()
311{
312	local certs1 diginotar_base diginotar evilcertsdir evildistrustdir
313
314	certs1=$(atf_get_srcdir)/certs1
315	diginotar_base=Explicitly_Distrust_DigiNotar_Root_CA.pem
316	diginotar=$certs1/$diginotar_base
317
318	evilcertsdir=$(printf '-evil certs\n.')
319	evilcertsdir=${evilcertsdir%.}
320	evildistrustdir=$(printf '-evil untrusted\n.')
321	evildistrustdir=${evildistrustdir%.}
322
323	setupconf certs1
324
325	# initial (re)hash, nonexistent certs directory
326	atf_check -s exit:0 $CERTCTL rehash
327	atf_check -s exit:0 certctl -C certs.conf \
328	    -c "$evilcertsdir" -u "$evildistrustdir" \
329	    rehash
330	atf_check -s exit:0 diff -ruN -- certs "$evilcertsdir"
331	atf_check -s exit:0 test ! -e untrusted
332	atf_check -s exit:0 test ! -h untrusted
333	atf_check -s exit:0 test ! -e "$evildistrustdir"
334	atf_check -s exit:0 test ! -h "$evildistrustdir"
335
336	# initial (re)hash, empty certs directory
337	atf_check -s exit:0 rm -rf -- certs
338	atf_check -s exit:0 rm -rf -- "$evilcertsdir"
339	atf_check -s exit:0 mkdir -- certs
340	atf_check -s exit:0 mkdir -- "$evilcertsdir"
341	atf_check -s exit:0 $CERTCTL rehash
342	atf_check -s exit:0 certctl -C certs.conf \
343	    -c "$evilcertsdir" -u "$evildistrustdir" \
344	    rehash
345	atf_check -s exit:0 diff -ruN -- certs "$evilcertsdir"
346	atf_check -s exit:0 test ! -e untrusted
347	atf_check -s exit:0 test ! -h untrusted
348	atf_check -s exit:0 test ! -e "$evildistrustdir"
349	atf_check -s exit:0 test ! -h "$evildistrustdir"
350
351	# test distrusting a CA
352	atf_check -s exit:0 $CERTCTL untrust "$diginotar"
353	atf_check -s exit:0 certctl -C certs.conf \
354	    -c "$evilcertsdir" -u "$evildistrustdir" \
355	    untrust "$diginotar"
356	atf_check -s exit:0 diff -ruN -- certs "$evilcertsdir"
357	atf_check -s exit:0 diff -ruN -- untrusted "$evildistrustdir"
358
359	# second rehash
360	atf_check -s exit:0 $CERTCTL rehash
361	atf_check -s exit:0 certctl -C certs.conf \
362	    -c "$evilcertsdir" -u "$evildistrustdir" \
363	    rehash
364	atf_check -s exit:0 diff -ruN -- certs "$evilcertsdir"
365	atf_check -s exit:0 diff -ruN -- untrusted "$evildistrustdir"
366}
367
368atf_test_case evilpath
369evilpath_head()
370{
371	atf_set "descr" "Test certificate paths with evil characters"
372}
373evilpath_body()
374{
375	local evildir
376
377	evildir=$(printf 'evil\n.')
378	evildir=${evildir%.}
379	mkdir "$evildir"
380
381	cp -p "$(atf_get_srcdir)/certs2"/*.pem "$evildir"/
382
383	setupconf certs1
384	cat <<EOF >>certs.conf
385path $(printf '%s' "$(pwd)/$evildir" | vis -M)
386EOF
387	checks certs1 "$(pwd)/$evildir"
388}
389
390atf_test_case missingconf
391missingconf_head()
392{
393	atf_set "descr" "Test certctl with missing certs.conf"
394}
395missingconf_body()
396{
397	mkdir certs
398	atf_check -s exit:0 test ! -e certs.conf
399	atf_check -s not-exit:0 -e match:'certs\.conf' \
400	    $CERTCTL rehash
401}
402
403atf_test_case nonexistentcertsdir
404nonexistentcertsdir_head()
405{
406	atf_set "descr" "Test certctl succeeds when certsdir is nonexistent"
407}
408nonexistentcertsdir_body()
409{
410	setupconf certs1
411	rmdir certs
412	checks certs1
413}
414
415atf_test_case symlinkcertsdir
416symlinkcertsdir_head()
417{
418	atf_set "descr" "Test certctl fails when certsdir is a symlink"
419}
420symlinkcertsdir_body()
421{
422	setupconf certs1
423	rmdir certs
424	mkdir empty
425	ln -sfn empty certs
426
427	atf_check -s not-exit:0 -e match:symlink $CERTCTL -n rehash
428	atf_check -s not-exit:0 -e match:symlink $CERTCTL rehash
429	atf_check -s exit:0 rmdir empty
430}
431
432atf_test_case regularfilecertsdir
433regularfilecertsdir_head()
434{
435	atf_set "descr" "Test certctl fails when certsdir is a regular file"
436}
437regularfilecertsdir_body()
438{
439	setupconf certs1
440	rmdir certs
441	echo 'hello world' >certs
442
443	atf_check -s not-exit:0 -e match:directory $CERTCTL -n rehash
444	atf_check -s not-exit:0 -e match:directory $CERTCTL rehash
445	atf_check -s exit:0 rm certs
446}
447
448atf_test_case prepopulatedcerts
449prepopulatedcerts_head()
450{
451	atf_set "descr" "Test certctl fails when directory is prepopulated"
452}
453prepopulatedcerts_body()
454{
455	local cert certbase target
456
457	setupconf certs1
458	ln -sfn "$(atf_get_srcdir)/certs2"/*.pem certs/
459
460	atf_check -s not-exit:0 -e match:manual $CERTCTL -n rehash
461	atf_check -s not-exit:0 -e match:manual $CERTCTL rehash
462	for cert in "$(atf_get_srcdir)/certs2"/*.pem; do
463		certbase=$(basename "$cert")
464		atf_check -s exit:0 -o inline:"$cert" \
465		    readlink -n "certs/$certbase"
466		rm "certs/$certbase"
467	done
468	check_empty
469}
470
471atf_init_test_cases()
472{
473	atf_add_test_case collidebase
474	atf_add_test_case collidehash
475	atf_add_test_case empty
476	atf_add_test_case evilcertsdir
477	atf_add_test_case evilpath
478	atf_add_test_case manual
479	atf_add_test_case missingconf
480	atf_add_test_case nonexistentcertsdir
481	atf_add_test_case onedir
482	atf_add_test_case prepopulatedcerts
483	atf_add_test_case regularfilecertsdir
484	atf_add_test_case symlinkcertsdir
485	atf_add_test_case twodir
486}
487