t_certctl.sh revision 1.1 1 #!/bin/sh
2
3 # $NetBSD: t_certctl.sh,v 1.1 2023/08/26 05:27:14 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 and delete it.
154 #
155 # XXX Verify its content.
156 atf_check -s exit:0 test -f certs/ca-certificates.crt
157 atf_check -s exit:0 test ! -h certs/ca-certificates.crt
158 rm certs/ca-certificates.crt
159
160 # Make sure after deleting everything there's nothing left.
161 check_empty "removing all expected certificates"
162
163 # Distrust, trust, and re-distrust one CA, and verify that it
164 # ceases to appear, reappears, and again ceases to appear.
165 # (This one has no subject hash collisions to worry about, so
166 # we hard-code the `.0' suffix.)
167 atf_check -s exit:0 $CERTCTL untrust "$diginotar"
168 atf_check -s exit:0 test -e "untrusted/$diginotar_base"
169 atf_check -s exit:0 test -h "untrusted/$diginotar_base"
170 atf_check -s exit:0 test ! -e "certs/$diginotar_base"
171 atf_check -s exit:0 test ! -h "certs/$diginotar_base"
172 atf_check -s exit:0 test ! -e "certs/$diginotar_hash.0"
173 atf_check -s exit:0 test ! -h "certs/$diginotar_hash.0"
174 check_nonempty
175
176 atf_check -s exit:0 $CERTCTL trust "$diginotar"
177 atf_check -s exit:0 test ! -e "untrusted/$diginotar_base"
178 atf_check -s exit:0 test ! -h "untrusted/$diginotar_base"
179 atf_check -s exit:0 test -e "certs/$diginotar_base"
180 atf_check -s exit:0 test -h "certs/$diginotar_base"
181 atf_check -s exit:0 test -e "certs/$diginotar_hash.0"
182 atf_check -s exit:0 test -h "certs/$diginotar_hash.0"
183 rm "certs/$diginotar_base"
184 rm "certs/$diginotar_hash.0"
185 check_nonempty
186
187 atf_check -s exit:0 $CERTCTL untrust "$diginotar"
188 atf_check -s exit:0 test -e "untrusted/$diginotar_base"
189 atf_check -s exit:0 test -h "untrusted/$diginotar_base"
190 atf_check -s exit:0 test ! -e "certs/$diginotar_base"
191 atf_check -s exit:0 test ! -h "certs/$diginotar_base"
192 atf_check -s exit:0 test ! -e "certs/$diginotar_hash.0"
193 atf_check -s exit:0 test ! -h "certs/$diginotar_hash.0"
194 check_nonempty
195 }
196
197 atf_test_case empty
198 empty_head()
199 {
200 atf_set "descr" "Test empty certificates store"
201 }
202 empty_body()
203 {
204 setupconf # no directories
205 check_empty "empty cert path"
206 atf_check -s exit:0 $CERTCTL -n rehash
207 check_empty
208 atf_check -s exit:0 $CERTCTL rehash
209 atf_check -s exit:0 test -f certs/ca-certificates.crt
210 atf_check -s exit:0 test \! -h certs/ca-certificates.crt
211 atf_check -s exit:0 test \! -s certs/ca-certificates.crt
212 atf_check -s exit:0 rm certs/ca-certificates.crt
213 check_empty "empty cert path"
214 }
215
216 atf_test_case onedir
217 onedir_head()
218 {
219 atf_set "descr" "Test one certificates directory"
220 }
221 onedir_body()
222 {
223 setupconf certs1
224 checks certs1
225 }
226
227 atf_test_case twodir
228 twodir_head()
229 {
230 atf_set "descr" "Test two certificates directories"
231 }
232 twodir_body()
233 {
234 setupconf certs1 certs2
235 checks certs1 certs2
236 }
237
238 atf_test_case collidehash
239 collidehash_head()
240 {
241 atf_set "descr" "Test colliding hashes"
242 }
243 collidehash_body()
244 {
245 # certs3 has two certificates with the same subject hash
246 setupconf certs1 certs3
247 checks certs1 certs3
248 }
249
250 atf_test_case collidebase
251 collidebase_head()
252 {
253 atf_set "descr" "Test colliding base names"
254 }
255 collidebase_body()
256 {
257 # certs1 and certs4 both have DigiCert_Global_Root_CA.pem,
258 # which should cause list and rehash to fail and mention
259 # duplicates.
260 setupconf certs1 certs4
261 atf_check -s not-exit:0 -o ignore -e match:duplicate $CERTCTL list
262 atf_check -s not-exit:0 -o ignore -e match:duplicate $CERTCTL rehash
263 }
264
265 atf_test_case manual
266 manual_head()
267 {
268 atf_set "descr" "Test manual operation"
269 }
270 manual_body()
271 {
272 local certs1 diginotar_base diginotar diginotar_hash
273
274 certs1=$(atf_get_srcdir)/certs1
275 diginotar_base=Explicitly_Distrust_DigiNotar_Root_CA.pem
276 diginotar=$certs1/$diginotar_base
277 diginotar_hash=$(openssl x509 -hash -noout <$diginotar)
278
279 setupconf certs1 certs2
280 cat <<EOF >>certs.conf
281 manual
282 EOF
283 touch certs/bogus.pem
284 ln -s bogus.pem certs/0123abcd.0
285
286 # Listing shouldn't mention anything in the certs/ cache.
287 atf_check -s exit:0 -o not-match:bogus $CERTCTL list
288 atf_check -s exit:0 -o not-match:bogus $CERTCTL untrusted
289
290 # Rehashing and changing the configuration should succeed, but
291 # mention `manual' in a warning message and should not touch
292 # the cache.
293 atf_check -s exit:0 -e match:manual $CERTCTL rehash
294 atf_check -s exit:0 -e match:manual $CERTCTL untrust "$diginotar"
295 atf_check -s exit:0 -e match:manual $CERTCTL trust "$diginotar"
296
297 # The files we created should still be there.
298 atf_check -s exit:0 test -f certs/bogus.pem
299 atf_check -s exit:0 test -h certs/0123abcd.0
300 }
301
302 atf_test_case evilpath
303 evilpath_head()
304 {
305 atf_set "descr" "Test certificate paths with evil characters"
306 }
307 evilpath_body()
308 {
309 local evildir
310
311 evildir="$(printf 'evil\n.')"
312 evildir=${evildir%.}
313 mkdir "$evildir"
314
315 cp -p "$(atf_get_srcdir)/certs2"/*.pem "$evildir"/
316
317 setupconf certs1
318 cat <<EOF >>certs.conf
319 path $(printf '%s' "$(pwd)/$evildir" | vis -M)
320 EOF
321 checks certs1 "$(pwd)/$evildir"
322 }
323
324 atf_init_test_cases()
325 {
326 atf_add_test_case collidebase
327 atf_add_test_case collidehash
328 atf_add_test_case empty
329 atf_add_test_case evilpath
330 atf_add_test_case manual
331 atf_add_test_case onedir
332 atf_add_test_case twodir
333 }
334