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