Home | History | Annotate | Line # | Download | only in certctl
      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 
     30 CERTCTL="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 #
     37 setupconf()
     38 {
     39 	local sep subdir dir
     40 
     41 	mkdir certs
     42 	cat <<EOF >certs.conf
     43 netbsd-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
     49 EOF
     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
     56 path$sep$(printf '%s' "$dir" | vis -M)
     57 EOF
     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 #
     67 check_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 #
     83 check_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 #
     95 checks()
     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 
    200 atf_test_case empty
    201 empty_head()
    202 {
    203 	atf_set "descr" "Test empty certificates store"
    204 }
    205 empty_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 
    219 atf_test_case onedir
    220 onedir_head()
    221 {
    222 	atf_set "descr" "Test one certificates directory"
    223 }
    224 onedir_body()
    225 {
    226 	setupconf certs1
    227 	checks certs1
    228 }
    229 
    230 atf_test_case twodir
    231 twodir_head()
    232 {
    233 	atf_set "descr" "Test two certificates directories"
    234 }
    235 twodir_body()
    236 {
    237 	setupconf certs1 certs2
    238 	checks certs1 certs2
    239 }
    240 
    241 atf_test_case collidehash
    242 collidehash_head()
    243 {
    244 	atf_set "descr" "Test colliding hashes"
    245 }
    246 collidehash_body()
    247 {
    248 	# certs3 has two certificates with the same subject hash
    249 	setupconf certs1 certs3
    250 	checks certs1 certs3
    251 }
    252 
    253 atf_test_case collidebase
    254 collidebase_head()
    255 {
    256 	atf_set "descr" "Test colliding base names"
    257 }
    258 collidebase_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 
    268 atf_test_case manual
    269 manual_head()
    270 {
    271 	atf_set "descr" "Test manual operation"
    272 }
    273 manual_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
    284 manual
    285 EOF
    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 
    305 atf_test_case evilcertsdir
    306 evilcertsdir_head()
    307 {
    308 	atf_set "descr" "Test certificate directory with evil characters"
    309 }
    310 evilcertsdir_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 
    368 atf_test_case evilpath
    369 evilpath_head()
    370 {
    371 	atf_set "descr" "Test certificate paths with evil characters"
    372 }
    373 evilpath_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
    385 path $(printf '%s' "$(pwd)/$evildir" | vis -M)
    386 EOF
    387 	checks certs1 "$(pwd)/$evildir"
    388 }
    389 
    390 atf_test_case missingconf
    391 missingconf_head()
    392 {
    393 	atf_set "descr" "Test certctl with missing certs.conf"
    394 }
    395 missingconf_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 
    403 atf_test_case nonexistentcertsdir
    404 nonexistentcertsdir_head()
    405 {
    406 	atf_set "descr" "Test certctl succeeds when certsdir is nonexistent"
    407 }
    408 nonexistentcertsdir_body()
    409 {
    410 	setupconf certs1
    411 	rmdir certs
    412 	checks certs1
    413 }
    414 
    415 atf_test_case symlinkcertsdir
    416 symlinkcertsdir_head()
    417 {
    418 	atf_set "descr" "Test certctl fails when certsdir is a symlink"
    419 }
    420 symlinkcertsdir_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 
    432 atf_test_case regularfilecertsdir
    433 regularfilecertsdir_head()
    434 {
    435 	atf_set "descr" "Test certctl fails when certsdir is a regular file"
    436 }
    437 regularfilecertsdir_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 
    448 atf_test_case prepopulatedcerts
    449 prepopulatedcerts_head()
    450 {
    451 	atf_set "descr" "Test certctl fails when directory is prepopulated"
    452 }
    453 prepopulatedcerts_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 
    471 atf_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