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