t_certctl.sh revision 1.8.2.3 1 #!/bin/sh
2
3 # $NetBSD: t_certctl.sh,v 1.8.2.3 2023/09/06 15:04:33 martin 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