aboutsummaryrefslogtreecommitdiff
path: root/usr.sbin/certctl/t_certctl.sh
diff options
context:
space:
mode:
Diffstat (limited to 'usr.sbin/certctl/t_certctl.sh')
-rw-r--r--usr.sbin/certctl/t_certctl.sh486
1 files changed, 486 insertions, 0 deletions
diff --git a/usr.sbin/certctl/t_certctl.sh b/usr.sbin/certctl/t_certctl.sh
new file mode 100644
index 000000000000..d14a57283404
--- /dev/null
+++ b/usr.sbin/certctl/t_certctl.sh
@@ -0,0 +1,486 @@
+#!/bin/sh
+
+# $NetBSD: t_certctl.sh,v 1.10 2023/09/05 12:32:30 riastradh Exp $
+#
+# Copyright (c) 2023 The NetBSD Foundation, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+CERTCTL="certctl -C certs.conf -c certs -u untrusted"
+
+# setupconf <subdir>...
+#
+# Create certs/ and set up certs.conf to search the specified
+# subdirectories of the source directory.
+#
+setupconf()
+{
+ local sep subdir dir
+
+ mkdir certs
+ cat <<EOF >certs.conf
+netbsd-certctl 20230816
+
+# comment at line start
+ # comment not at line start, plus some intentional whitespace
+
+# THE WHITESPACE ABOVE IS INTENTIONAL, DO NOT DELETE
+EOF
+ # Start with a continuation line separator; then switch to
+ # non-continuation lines.
+ sep=$(printf ' \\\n\t')
+ for subdir; do
+ dir=$(atf_get_srcdir)/$subdir
+ cat <<EOF >>certs.conf
+path$sep$(printf '%s' "$dir" | vis -M)
+EOF
+ sep=' '
+ done
+}
+
+# check_empty
+#
+# Verify the certs directory is empty after dry runs or after
+# clearing the directory.
+#
+check_empty()
+{
+ local why
+
+ why=${1:-dry run}
+ for x in certs/*; do
+ if [ -e "$x" -o -h "$x" ]; then
+ atf_fail "certs/ should be empty after $why"
+ fi
+ done
+}
+
+# check_nonempty
+#
+# Verify the certs directory is nonempty.
+#
+check_nonempty()
+{
+ for x in certs/*.0; do
+ test -e "$x" && test -h "$x" && return
+ done
+ atf_fail "certs/ should be nonempty"
+}
+
+# checks <certsN>...
+#
+# Run various checks with certctl.
+#
+checks()
+{
+ local certs1 diginotar_base diginotar diginotar_hash subdir srcdir
+
+ certs1=$(atf_get_srcdir)/certs1
+ diginotar_base=Explicitly_Distrust_DigiNotar_Root_CA.pem
+ diginotar=$certs1/$diginotar_base
+ diginotar_hash=$(openssl x509 -hash -noout <$diginotar)
+
+ # Do a dry run of rehash and make sure the directory is still
+ # empty.
+ atf_check -s exit:0 $CERTCTL -n rehash
+ check_empty
+
+ # Distrust and trust one CA, as a dry run. The trust should
+ # fail because it's not currently distrusted.
+ atf_check -s exit:0 $CERTCTL -n untrust "$diginotar"
+ check_empty
+ atf_check -s not-exit:0 -e match:currently \
+ $CERTCTL -n trust "$diginotar"
+ check_empty
+
+ # Do a real rehash, not a dry run.
+ atf_check -s exit:0 $CERTCTL rehash
+
+ # Make sure all the certificates are trusted.
+ for subdir; do
+ case $subdir in
+ /*) srcdir=$subdir;;
+ *) srcdir=$(atf_get_srcdir)/$subdir;;
+ esac
+ for cert in "$srcdir"/*.pem; do
+ # Verify the certificate is linked by its base name.
+ certbase=$(basename "$cert")
+ atf_check -s exit:0 -o inline:"$cert" \
+ readlink -n "certs/$certbase"
+
+ # Verify the certificate is linked by a hash.
+ hash=$(openssl x509 -hash -noout <$cert)
+ counter=0
+ found=false
+ while [ $counter -lt 10 ]; do
+ if cmp -s "certs/$hash.$counter" "$cert"; then
+ found=true
+ break
+ fi
+ counter=$((counter + 1))
+ done
+ if ! $found; then
+ atf_fail "missing $cert"
+ fi
+
+ # Delete both links.
+ rm "certs/$certbase"
+ rm "certs/$hash.$counter"
+ done
+ done
+
+ # Verify the certificate bundle is there with the right
+ # permissions (0644) and delete it.
+ #
+ # XXX Verify its content.
+ atf_check -s exit:0 test -f certs/ca-certificates.crt
+ atf_check -s exit:0 test ! -h certs/ca-certificates.crt
+ atf_check -s exit:0 -o inline:'100644\n' \
+ stat -f %p certs/ca-certificates.crt
+ rm certs/ca-certificates.crt
+
+ # Make sure after deleting everything there's nothing left.
+ check_empty "removing all expected certificates"
+
+ # Distrust, trust, and re-distrust one CA, and verify that it
+ # ceases to appear, reappears, and again ceases to appear.
+ # (This one has no subject hash collisions to worry about, so
+ # we hard-code the `.0' suffix.)
+ atf_check -s exit:0 $CERTCTL untrust "$diginotar"
+ atf_check -s exit:0 test -e "untrusted/$diginotar_base"
+ atf_check -s exit:0 test -h "untrusted/$diginotar_base"
+ atf_check -s exit:0 test ! -e "certs/$diginotar_base"
+ atf_check -s exit:0 test ! -h "certs/$diginotar_base"
+ atf_check -s exit:0 test ! -e "certs/$diginotar_hash.0"
+ atf_check -s exit:0 test ! -h "certs/$diginotar_hash.0"
+ check_nonempty
+
+ atf_check -s exit:0 $CERTCTL trust "$diginotar"
+ atf_check -s exit:0 test ! -e "untrusted/$diginotar_base"
+ atf_check -s exit:0 test ! -h "untrusted/$diginotar_base"
+ atf_check -s exit:0 test -e "certs/$diginotar_base"
+ atf_check -s exit:0 test -h "certs/$diginotar_base"
+ atf_check -s exit:0 test -e "certs/$diginotar_hash.0"
+ atf_check -s exit:0 test -h "certs/$diginotar_hash.0"
+ rm "certs/$diginotar_base"
+ rm "certs/$diginotar_hash.0"
+ check_nonempty
+
+ atf_check -s exit:0 $CERTCTL untrust "$diginotar"
+ atf_check -s exit:0 test -e "untrusted/$diginotar_base"
+ atf_check -s exit:0 test -h "untrusted/$diginotar_base"
+ atf_check -s exit:0 test ! -e "certs/$diginotar_base"
+ atf_check -s exit:0 test ! -h "certs/$diginotar_base"
+ atf_check -s exit:0 test ! -e "certs/$diginotar_hash.0"
+ atf_check -s exit:0 test ! -h "certs/$diginotar_hash.0"
+ check_nonempty
+}
+
+atf_test_case empty
+empty_head()
+{
+ atf_set "descr" "Test empty certificates store"
+}
+empty_body()
+{
+ setupconf # no directories
+ check_empty "empty cert path"
+ atf_check -s exit:0 $CERTCTL -n rehash
+ check_empty
+ atf_check -s exit:0 $CERTCTL rehash
+ atf_check -s exit:0 test -f certs/ca-certificates.crt
+ atf_check -s exit:0 test \! -h certs/ca-certificates.crt
+ atf_check -s exit:0 test \! -s certs/ca-certificates.crt
+ atf_check -s exit:0 rm certs/ca-certificates.crt
+ check_empty "empty cert path"
+}
+
+atf_test_case onedir
+onedir_head()
+{
+ atf_set "descr" "Test one certificates directory"
+}
+onedir_body()
+{
+ setupconf certs1
+ checks certs1
+}
+
+atf_test_case twodir
+twodir_head()
+{
+ atf_set "descr" "Test two certificates directories"
+}
+twodir_body()
+{
+ setupconf certs1 certs2
+ checks certs1 certs2
+}
+
+atf_test_case collidehash
+collidehash_head()
+{
+ atf_set "descr" "Test colliding hashes"
+}
+collidehash_body()
+{
+ # certs3 has two certificates with the same subject hash
+ setupconf certs1 certs3
+ checks certs1 certs3
+}
+
+atf_test_case collidebase
+collidebase_head()
+{
+ atf_set "descr" "Test colliding base names"
+}
+collidebase_body()
+{
+ # certs1 and certs4 both have DigiCert_Global_Root_CA.pem,
+ # which should cause list and rehash to fail and mention
+ # duplicates.
+ setupconf certs1 certs4
+ atf_check -s not-exit:0 -o ignore -e match:duplicate $CERTCTL list
+ atf_check -s not-exit:0 -o ignore -e match:duplicate $CERTCTL rehash
+}
+
+atf_test_case manual
+manual_head()
+{
+ atf_set "descr" "Test manual operation"
+}
+manual_body()
+{
+ local certs1 diginotar_base diginotar diginotar_hash
+
+ certs1=$(atf_get_srcdir)/certs1
+ diginotar_base=Explicitly_Distrust_DigiNotar_Root_CA.pem
+ diginotar=$certs1/$diginotar_base
+ diginotar_hash=$(openssl x509 -hash -noout <$diginotar)
+
+ setupconf certs1 certs2
+ cat <<EOF >>certs.conf
+manual
+EOF
+ touch certs/bogus.pem
+ ln -s bogus.pem certs/0123abcd.0
+
+ # Listing shouldn't mention anything in the certs/ cache.
+ atf_check -s exit:0 -o not-match:bogus $CERTCTL list
+ atf_check -s exit:0 -o not-match:bogus $CERTCTL untrusted
+
+ # Rehashing and changing the configuration should succeed, but
+ # mention `manual' in a warning message and should not touch
+ # the cache.
+ atf_check -s exit:0 -e match:manual $CERTCTL rehash
+ atf_check -s exit:0 -e match:manual $CERTCTL untrust "$diginotar"
+ atf_check -s exit:0 -e match:manual $CERTCTL trust "$diginotar"
+
+ # The files we created should still be there.
+ atf_check -s exit:0 test -f certs/bogus.pem
+ atf_check -s exit:0 test -h certs/0123abcd.0
+}
+
+atf_test_case evilcertsdir
+evilcertsdir_head()
+{
+ atf_set "descr" "Test certificate directory with evil characters"
+}
+evilcertsdir_body()
+{
+ local certs1 diginotar_base diginotar evilcertsdir evildistrustdir
+
+ certs1=$(atf_get_srcdir)/certs1
+ diginotar_base=Explicitly_Distrust_DigiNotar_Root_CA.pem
+ diginotar=$certs1/$diginotar_base
+
+ evilcertsdir=$(printf '-evil certs\n.')
+ evilcertsdir=${evilcertsdir%.}
+ evildistrustdir=$(printf '-evil untrusted\n.')
+ evildistrustdir=${evildistrustdir%.}
+
+ setupconf certs1
+
+ # initial (re)hash, nonexistent certs directory
+ atf_check -s exit:0 $CERTCTL rehash
+ atf_check -s exit:0 certctl -C certs.conf \
+ -c "$evilcertsdir" -u "$evildistrustdir" \
+ rehash
+ atf_check -s exit:0 diff -ruN -- certs "$evilcertsdir"
+ atf_check -s exit:0 test ! -e untrusted
+ atf_check -s exit:0 test ! -h untrusted
+ atf_check -s exit:0 test ! -e "$evildistrustdir"
+ atf_check -s exit:0 test ! -h "$evildistrustdir"
+
+ # initial (re)hash, empty certs directory
+ atf_check -s exit:0 rm -rf -- certs
+ atf_check -s exit:0 rm -rf -- "$evilcertsdir"
+ atf_check -s exit:0 mkdir -- certs
+ atf_check -s exit:0 mkdir -- "$evilcertsdir"
+ atf_check -s exit:0 $CERTCTL rehash
+ atf_check -s exit:0 certctl -C certs.conf \
+ -c "$evilcertsdir" -u "$evildistrustdir" \
+ rehash
+ atf_check -s exit:0 diff -ruN -- certs "$evilcertsdir"
+ atf_check -s exit:0 test ! -e untrusted
+ atf_check -s exit:0 test ! -h untrusted
+ atf_check -s exit:0 test ! -e "$evildistrustdir"
+ atf_check -s exit:0 test ! -h "$evildistrustdir"
+
+ # test distrusting a CA
+ atf_check -s exit:0 $CERTCTL untrust "$diginotar"
+ atf_check -s exit:0 certctl -C certs.conf \
+ -c "$evilcertsdir" -u "$evildistrustdir" \
+ untrust "$diginotar"
+ atf_check -s exit:0 diff -ruN -- certs "$evilcertsdir"
+ atf_check -s exit:0 diff -ruN -- untrusted "$evildistrustdir"
+
+ # second rehash
+ atf_check -s exit:0 $CERTCTL rehash
+ atf_check -s exit:0 certctl -C certs.conf \
+ -c "$evilcertsdir" -u "$evildistrustdir" \
+ rehash
+ atf_check -s exit:0 diff -ruN -- certs "$evilcertsdir"
+ atf_check -s exit:0 diff -ruN -- untrusted "$evildistrustdir"
+}
+
+atf_test_case evilpath
+evilpath_head()
+{
+ atf_set "descr" "Test certificate paths with evil characters"
+}
+evilpath_body()
+{
+ local evildir
+
+ evildir=$(printf 'evil\n.')
+ evildir=${evildir%.}
+ mkdir "$evildir"
+
+ cp -p "$(atf_get_srcdir)/certs2"/*.pem "$evildir"/
+
+ setupconf certs1
+ cat <<EOF >>certs.conf
+path $(printf '%s' "$(pwd)/$evildir" | vis -M)
+EOF
+ checks certs1 "$(pwd)/$evildir"
+}
+
+atf_test_case missingconf
+missingconf_head()
+{
+ atf_set "descr" "Test certctl with missing certs.conf"
+}
+missingconf_body()
+{
+ mkdir certs
+ atf_check -s exit:0 test ! -e certs.conf
+ atf_check -s not-exit:0 -e match:'certs\.conf' \
+ $CERTCTL rehash
+}
+
+atf_test_case nonexistentcertsdir
+nonexistentcertsdir_head()
+{
+ atf_set "descr" "Test certctl succeeds when certsdir is nonexistent"
+}
+nonexistentcertsdir_body()
+{
+ setupconf certs1
+ rmdir certs
+ checks certs1
+}
+
+atf_test_case symlinkcertsdir
+symlinkcertsdir_head()
+{
+ atf_set "descr" "Test certctl fails when certsdir is a symlink"
+}
+symlinkcertsdir_body()
+{
+ setupconf certs1
+ rmdir certs
+ mkdir empty
+ ln -sfn empty certs
+
+ atf_check -s not-exit:0 -e match:symlink $CERTCTL -n rehash
+ atf_check -s not-exit:0 -e match:symlink $CERTCTL rehash
+ atf_check -s exit:0 rmdir empty
+}
+
+atf_test_case regularfilecertsdir
+regularfilecertsdir_head()
+{
+ atf_set "descr" "Test certctl fails when certsdir is a regular file"
+}
+regularfilecertsdir_body()
+{
+ setupconf certs1
+ rmdir certs
+ echo 'hello world' >certs
+
+ atf_check -s not-exit:0 -e match:directory $CERTCTL -n rehash
+ atf_check -s not-exit:0 -e match:directory $CERTCTL rehash
+ atf_check -s exit:0 rm certs
+}
+
+atf_test_case prepopulatedcerts
+prepopulatedcerts_head()
+{
+ atf_set "descr" "Test certctl fails when directory is prepopulated"
+}
+prepopulatedcerts_body()
+{
+ local cert certbase target
+
+ setupconf certs1
+ ln -sfn "$(atf_get_srcdir)/certs2"/*.pem certs/
+
+ atf_check -s not-exit:0 -e match:manual $CERTCTL -n rehash
+ atf_check -s not-exit:0 -e match:manual $CERTCTL rehash
+ for cert in "$(atf_get_srcdir)/certs2"/*.pem; do
+ certbase=$(basename "$cert")
+ atf_check -s exit:0 -o inline:"$cert" \
+ readlink -n "certs/$certbase"
+ rm "certs/$certbase"
+ done
+ check_empty
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case collidebase
+ atf_add_test_case collidehash
+ atf_add_test_case empty
+ atf_add_test_case evilcertsdir
+ atf_add_test_case evilpath
+ atf_add_test_case manual
+ atf_add_test_case missingconf
+ atf_add_test_case nonexistentcertsdir
+ atf_add_test_case onedir
+ atf_add_test_case prepopulatedcerts
+ atf_add_test_case regularfilecertsdir
+ atf_add_test_case symlinkcertsdir
+ atf_add_test_case twodir
+}