Merge remote-tracking branch 'aosp/upstream-master'

Upgrade to fsverity-utils v1.5 so that AOSP is on a tagged release
version.  No significant changes since the last merge.

* aosp/upstream-master:
  v1.5
  NEWS.md: update for v1.5
  scripts/do-release.sh: split into prepare and publish
  scripts/run-sparse.sh: fix to exclude boringssl directory
  Clarify the purpose of built-in signatures
  Makefile: fix a typo
  Add GitHub Actions support
  Support automatically building BoringSSL for testing
  run-tests.sh: make CFI test work on Ubuntu 20.04
  run-tests.sh: allow running individual tests

Test: mmm external/fsverity-utils
Change-Id: Icdf6279c9bdaed4cc5a87aabaf444e5d179ff089
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..309013a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,169 @@
+# SPDX-License-Identifier: MIT
+# Copyright 2021 Google LLC
+#
+# Use of this source code is governed by an MIT-style
+# license that can be found in the LICENSE file or at
+# https://opensource.org/licenses/MIT.
+
+name: CI
+on: [pull_request]
+
+jobs:
+  static-linking-test:
+    name: Test building static library
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - run: scripts/run-tests.sh static_linking
+
+  dynamic-linking-test:
+    name: Test building dynamic library
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - run: scripts/run-tests.sh dynamic_linking
+
+  cplusplus-test:
+    name: Test using library from C++ program
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - run: scripts/run-tests.sh cplusplus
+
+  uninstall-test:
+    name: Test uninstalling
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - run: scripts/run-tests.sh uninstall
+
+  dash-test:
+    name: Test building using the dash shell
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - run: scripts/run-tests.sh dash
+
+  license-test:
+    name: Test for correct license info
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - run: scripts/run-tests.sh license
+
+  gcc-test:
+    name: Test with gcc
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - run: scripts/run-tests.sh gcc
+
+  clang-test:
+    name: Test with clang
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y clang
+    - run: scripts/run-tests.sh clang
+
+  _32bit-test:
+    name: Test building 32-bit binaries
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Install dependencies
+      run: |
+        sudo dpkg --add-architecture i386
+        sudo apt-get update
+        sudo apt-get install -y gcc-multilib libssl-dev:i386
+    - run: scripts/run-tests.sh 32bit
+
+  sanitizers-test:
+    name: Test with sanitizers enabled
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y clang llvm
+    - run: scripts/run-tests.sh sanitizers
+
+  valgrind-test:
+    name: Test with valgrind enabled
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y valgrind
+    - run: scripts/run-tests.sh valgrind
+
+  boringssl-test:
+    name: Test with BoringSSL
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Cache BoringSSL build
+      uses: actions/cache@v2
+      with:
+        key: boringssl
+        path: boringssl
+    - run: make boringssl
+    - run: scripts/run-tests.sh boringssl
+
+  char-test:
+    name: Test with unsigned/signed char
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - run: scripts/run-tests.sh unsigned_char signed_char
+
+  # FIXME: need a Windows build of libcrypto for this to work
+  #windows-build-test:
+    #name: Windows build tests
+    #runs-on: ubuntu-latest
+    #steps:
+    #- uses: actions/checkout@v2
+    #- name: Install dependencies
+      #run: |
+        #sudo apt-get update
+        #sudo apt-get install -y gcc-mingw-w64-i686 gcc-mingw-w64-x86-64
+    # - run: scripts/run-tests.sh windows_build
+
+  sparse-test:
+    name: Run sparse
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y sparse
+    - run: scripts/run-tests.sh sparse
+
+  clang-analyzer-test:
+    name: Run clang static analyzer
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y clang-tools
+    - run: scripts/run-tests.sh clang_analyzer
+
+  shellcheck-test:
+    name: Run shellcheck
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y shellcheck
+    - run: scripts/run-tests.sh shellcheck
diff --git a/.gitignore b/.gitignore
index eba6ab6..3ea5ca6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@
 *.so
 *.so.*
 /.build-config
+/boringssl
+/boringssl.tar.gz
 /fsverity
 /fsverity.sig
 /run-tests.log
diff --git a/METADATA b/METADATA
index 52ec89e..b0d1913 100644
--- a/METADATA
+++ b/METADATA
@@ -5,12 +5,12 @@
     type: GIT
     value: "https://git.kernel.org/pub/scm/linux/kernel/git/ebiggers/fsverity-utils.git"
   }
-  version: "4258209301d54512956d536149b0eef0c695cfe6"
+  version: "20e87c13075a8e5660a8d69fd6c93d4f7c5f01a5"
   # would be NOTICE save for common/fsverity_uapi.h
   license_type: RESTRICTED
   last_upgrade_date {
-    year: 2021
-    month: 12
-    day: 20
+    year: 2022
+    month: 2
+    day: 7
   }
 }
diff --git a/Makefile b/Makefile
index 81b7b6d..2304a21 100644
--- a/Makefile
+++ b/Makefile
@@ -46,7 +46,7 @@
 
 # Set the CFLAGS.  First give the warning-related flags (unconditionally, though
 # the user can override any of them by specifying the opposite flag); then give
-# the user-specifed CFLAGS, defaulting to -O2 if none were specified.
+# the user-specified CFLAGS, defaulting to -O2 if none were specified.
 #
 # Use -Wno-deprecated-declarations to avoid warnings about the Engine API having
 # been deprecated in OpenSSL 3.0; the replacement isn't ready yet.
@@ -213,6 +213,21 @@
 
 ##############################################################################
 
+# Support for downloading and building BoringSSL.  The purpose of this is to
+# allow testing builds of fsverity-utils that link to BoringSSL instead of
+# OpenSSL, without having to use a system that uses BoringSSL natively.
+
+boringssl:
+	rm -rf boringssl boringssl.tar.gz
+	curl -s -o boringssl.tar.gz \
+		https://boringssl.googlesource.com/boringssl/+archive/refs/heads/master.tar.gz
+	mkdir boringssl
+	tar xf boringssl.tar.gz -C boringssl
+	cmake -B boringssl/build boringssl
+	$(MAKE) -C boringssl/build $(MAKEFLAGS)
+
+##############################################################################
+
 SPECIAL_TARGETS := all test_programs check install install-man uninstall \
 		   help clean
 
diff --git a/NEWS.md b/NEWS.md
index be8d12f..c63dff9 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,16 @@
 # fsverity-utils release notes
 
+## Version 1.5
+
+* Made the `fsverity sign` command and the `libfsverity_sign_digest()` function
+  support PKCS#11 tokens.
+
+* Avoided a compiler error when building with musl libc.
+
+* Avoided compiler warnings when building with OpenSSL 3.0.
+
+* Improved documentation and test scripts.
+
 ## Version 1.4
 
 * Added a manual page for the `fsverity` utility.
diff --git a/README.md b/README.md
index 14c7bbe..ffa25fd 100644
--- a/README.md
+++ b/README.md
@@ -102,11 +102,44 @@
 
 ### Using builtin signatures
 
-With `CONFIG_FS_VERITY_BUILTIN_SIGNATURES=y`, the filesystem supports
-automatically verifying a signed file digest that has been included in
-the verity metadata.  The signature is verified against the set of
-X.509 certificates that have been loaded into the ".fs-verity" kernel
-keyring.  Here's an example:
+First, note that fs-verity is essentially just a way of hashing a
+file; it doesn't mandate a specific way of handling signatures.
+There are several possible ways that signatures could be handled:
+
+* Do it entirely in userspace
+* Use IMA appraisal (work-in-progress)
+* Use fs-verity built-in signatures
+
+Any such solution needs two parts: (a) a policy that determines which
+files are required to have fs-verity enabled and have a valid
+signature, and (b) enforcement of the policy.  Each part could happen
+either in a trusted userspace program(s) or in the kernel.
+
+fs-verity built-in signatures (which are supported when the kernel was
+built with `CONFIG_FS_VERITY_BUILTIN_SIGNATURES=y`) are a hybrid
+solution where the policy of which files are required to be signed is
+determined and enforced by a trusted userspace program, but the actual
+signature verification happens in the kernel.  Specifically, with
+built-in signatures, the filesystem supports storing a signed file
+digest in each file's verity metadata.  Before allowing access to the
+file, the filesystem will automatically verify the signature against
+the set of X.509 certificates in the ".fs-verity" kernel keyring.  If
+set, the sysctl `fs.verity.require_signatures=1` will make the kernel
+enforce that every verity file has a valid built-in signature.
+
+fs-verity built-in signatures are primarily intended as a
+proof-of-concept; they reuse the kernel code that verifies the
+signatures of loadable kernel modules.  This solution still requires a
+trusted userspace program to enforce that particular files have
+fs-verity enabled.  Also, this solution uses PKCS#7 signatures, which
+are complex and prone to security bugs.
+
+Thus, if possible one of the other solutions should be used instead.
+For example, the trusted userspace program could verify signatures
+itself, using a simple signature format using a modern algorithm such
+as Ed25519.
+
+That being said, here are some examples of using built-in signatures:
 
 ```bash
     # Generate a new certificate and private key:
@@ -137,15 +170,6 @@
     fsverity digest file --compact --for-builtin-sig | tr -d '\n' | xxd -p -r | openssl smime -sign -in /dev/stdin ...
 ```
 
-By default, it's not required that verity files have a signature.
-This can be changed with `sysctl fs.verity.require_signatures=1`.
-When set, it's guaranteed that the contents of every verity file has
-been signed by one of the certificates in the keyring.
-
-Note: applications generally still need to check whether the file
-they're accessing really is a verity file, since an attacker could
-replace a verity file with a regular one.
-
 ### With IMA
 
 IMA support for fs-verity is planned.
diff --git a/include/libfsverity.h b/include/libfsverity.h
index fe89371..a0a1527 100644
--- a/include/libfsverity.h
+++ b/include/libfsverity.h
@@ -22,7 +22,7 @@
 #include <stdint.h>
 
 #define FSVERITY_UTILS_MAJOR_VERSION	1
-#define FSVERITY_UTILS_MINOR_VERSION	4
+#define FSVERITY_UTILS_MINOR_VERSION	5
 
 #define FS_VERITY_HASH_ALG_SHA256       1
 #define FS_VERITY_HASH_ALG_SHA512       2
@@ -186,13 +186,13 @@
 			   struct libfsverity_digest **digest_ret);
 
 /**
- * libfsverity_sign_digest() - Sign previously computed digest of a file
- *          This signature is used by the filesystem to validate the signed file
- *          digest against a public key loaded into the .fs-verity kernel
- *          keyring, when CONFIG_FS_VERITY_BUILTIN_SIGNATURES is enabled. The
- *          signature is formatted as PKCS#7 stored in DER format. See
- *          Documentation/filesystems/fsverity.rst in the kernel source tree for
- *          further details.
+ * libfsverity_sign_digest() - Sign a file for built-in signature verification
+ *	    Sign a file digest in a way that is compatible with the Linux
+ *	    kernel's fs-verity built-in signature verification support.  The
+ *	    resulting signature will be a PKCS#7 message in DER format.  Note
+ *	    that this is not the only way to do signatures with fs-verity.  For
+ *	    more details, refer to the fsverity-utils README and to
+ *	    Documentation/filesystems/fsverity.rst in the kernel source tree.
  * @digest: pointer to previously computed digest
  * @sig_params: pointer to the certificate and private key information
  * @sig_ret: Pointer to pointer for signed digest
diff --git a/lib/libfsverity.pc.in b/lib/libfsverity.pc.in
index 03496fe..4c9bb20 100644
--- a/lib/libfsverity.pc.in
+++ b/lib/libfsverity.pc.in
@@ -4,7 +4,7 @@
 
 Name: libfsverity
 Description: fs-verity library
-Version: 1.4
+Version: 1.5
 Libs: -L${libdir} -lfsverity
 Requires.private: libcrypto
 Cflags: -I${includedir}
diff --git a/man/fsverity.1.md b/man/fsverity.1.md
index a983912..dd54964 100644
--- a/man/fsverity.1.md
+++ b/man/fsverity.1.md
@@ -1,6 +1,6 @@
-% FSVERITY(1) fsverity-utils v1.4 | User Commands
+% FSVERITY(1) fsverity-utils v1.5 | User Commands
 %
-% June 2021
+% February 2022
 
 # NAME
 
@@ -161,6 +161,10 @@
 optionally **\-\-pkcs11-keyid**.  PKCS#11 token support is unavailable when
 fsverity-utils was built with BoringSSL rather than OpenSSL.
 
+**fsverity sign** should only be used if you need compatibility with fs-verity
+built-in signatures.  It is not the only way to do signatures with fs-verity.
+For more information, see the fsverity-utils README.
+
 Options accepted by **fsverity sign**:
 
 **\-\-block-size**=*BLOCK_SIZE*
diff --git a/programs/fsverity.c b/programs/fsverity.c
index 813ea2a..e4e348b 100644
--- a/programs/fsverity.c
+++ b/programs/fsverity.c
@@ -56,7 +56,7 @@
 	}, {
 		.name = "sign",
 		.func = fsverity_cmd_sign,
-		.short_desc = "Sign a file for fs-verity",
+		.short_desc = "Sign a file for fs-verity built-in signature verification",
 		.usage_str =
 "    fsverity sign FILE OUT_SIGFILE\n"
 "               [--key=KEYFILE] [--cert=CERTFILE] [--pkcs11-engine=SOFILE]\n"
diff --git a/scripts/do-release.sh b/scripts/do-release.sh
index 9f6bf73..3f68497 100755
--- a/scripts/do-release.sh
+++ b/scripts/do-release.sh
@@ -9,44 +9,73 @@
 set -e -u -o pipefail
 cd "$(dirname "$0")/.."
 
-if [ $# != 1 ]; then
-	echo "Usage: $0 VERS" 1>&2
-	echo "  e.g. $0 1.0" 1>&2
+usage()
+{
+	echo "Usage: $0 prepare|publish VERS" 1>&2
+	echo "  e.g. $0 prepare 1.0" 1>&2
+	echo "       $0 publish 1.0" 1>&2
 	exit 2
+}
+
+if [ $# != 2 ]; then
+	usage
 fi
 
-VERS=$1
+PUBLISH=false
+case $1 in
+publish)
+	PUBLISH=true
+	;;
+prepare)
+	;;
+*)
+	usage
+	;;
+esac
+VERS=$2
 PKG=fsverity-utils-$VERS
 
-git checkout -f
-git clean -fdx
-./scripts/run-tests.sh
-git clean -fdx
+prepare_release()
+{
+	git checkout -f
+	git clean -fdx
+	./scripts/run-tests.sh
+	git clean -fdx
 
-major=$(echo "$VERS" | cut -d. -f1)
-minor=$(echo "$VERS" | cut -d. -f2)
-month=$(LC_ALL=C date +%B)
-year=$(LC_ALL=C date +%Y)
+	major=$(echo "$VERS" | cut -d. -f1)
+	minor=$(echo "$VERS" | cut -d. -f2)
+	month=$(LC_ALL=C date +%B)
+	year=$(LC_ALL=C date +%Y)
 
-sed -E -i -e "/FSVERITY_UTILS_MAJOR_VERSION/s/[0-9]+/$major/" \
-	  -e "/FSVERITY_UTILS_MINOR_VERSION/s/[0-9]+/$minor/" \
-	  include/libfsverity.h
-sed -E -i "/Version:/s/[0-9]+\.[0-9]+/$VERS/" \
-	  lib/libfsverity.pc.in
-sed -E -i -e "/^% /s/fsverity-utils v[0-9]+(\.[0-9]+)+/fsverity-utils v$VERS/" \
-	  -e "/^% /s/[a-zA-Z]+ 2[0-9]{3}/$month $year/" \
-	  man/*.[1-9].md
-git commit -a --signoff --message="v$VERS"
-git tag --sign "v$VERS" --message="$PKG"
+	sed -E -i -e "/FSVERITY_UTILS_MAJOR_VERSION/s/[0-9]+/$major/" \
+		  -e "/FSVERITY_UTILS_MINOR_VERSION/s/[0-9]+/$minor/" \
+		  include/libfsverity.h
+	sed -E -i "/Version:/s/[0-9]+\.[0-9]+/$VERS/" \
+		  lib/libfsverity.pc.in
+	sed -E -i -e "/^% /s/fsverity-utils v[0-9]+(\.[0-9]+)+/fsverity-utils v$VERS/" \
+		  -e "/^% /s/[a-zA-Z]+ 2[0-9]{3}/$month $year/" \
+		  man/*.[1-9].md
+	git commit -a --signoff --message="v$VERS"
+	git tag --sign "v$VERS" --message="$PKG"
 
-git archive "v$VERS" --prefix="$PKG/" > "$PKG.tar"
-tar xf "$PKG.tar"
-( cd "$PKG" && make check )
-rm -r "$PKG"
+	git archive "v$VERS" --prefix="$PKG/" > "$PKG.tar"
+	tar xf "$PKG.tar"
+	( cd "$PKG" && make check )
+	rm -r "$PKG"
+}
 
-gpg --detach-sign --armor "$PKG.tar"
-DESTDIR=/pub/linux/kernel/people/ebiggers/fsverity-utils/v$VERS
-kup mkdir "$DESTDIR"
-kup put "$PKG.tar" "$PKG.tar.asc" "$DESTDIR/$PKG.tar.gz"
-git push
-git push --tags
+publish_release()
+{
+	gpg --detach-sign --armor "$PKG.tar"
+	DESTDIR=/pub/linux/kernel/people/ebiggers/fsverity-utils/v$VERS
+	kup mkdir "$DESTDIR"
+	kup put "$PKG.tar" "$PKG.tar.asc" "$DESTDIR/$PKG.tar.gz"
+	git push
+	git push --tags
+}
+
+if $PUBLISH; then
+	publish_release
+else
+	prepare_release
+fi
diff --git a/scripts/run-sparse.sh b/scripts/run-sparse.sh
index f75b837..b8d37c1 100755
--- a/scripts/run-sparse.sh
+++ b/scripts/run-sparse.sh
@@ -8,7 +8,7 @@
 
 set -e -u -o pipefail
 
-find . -name '*.c' | while read -r file; do
+find programs lib -name '*.c' | while read -r file; do
 	sparse "$file" -gcc-base-dir "$(gcc --print-file-name=)"	\
 		-Iinclude -D_FILE_OFFSET_BITS=64 -Wbitwise -D_GNU_SOURCE
 done
diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh
index fb21c39..e2a4e38 100755
--- a/scripts/run-tests.sh
+++ b/scripts/run-tests.sh
@@ -17,11 +17,13 @@
 set -e -u -o pipefail
 cd "$(dirname "$0")/.."
 
-log() {
+log()
+{
 	echo "[$(date)] $*" 1>&2
 }
 
-fail() {
+fail()
+{
 	echo "FAIL: $*" 1>&2
 	exit 1
 }
@@ -38,31 +40,44 @@
 
 MAKE="make -j$(getconf _NPROCESSORS_ONLN)"
 
-log "Build and test with statically linking"
-$MAKE CFLAGS="-Werror"
-if ldd fsverity | grep libfsverity.so; then
-	fail "fsverity binary should be statically linked to libfsverity by default"
-fi
-./fsverity --version
+TEST_FUNCS=()
 
-log "Check that all global symbols are prefixed with \"libfsverity_\""
-if nm libfsverity.a | grep ' T ' | grep -v " libfsverity_"; then
-	fail "Some global symbols are not prefixed with \"libfsverity_\""
-fi
+static_linking_test()
+{
+	log "Build and test with statically linking"
+	$MAKE CFLAGS="-Werror"
+	if ldd fsverity | grep libfsverity.so; then
+		fail "fsverity binary should be statically linked to libfsverity by default"
+	fi
+	./fsverity --version
 
-log "Build and test with dynamic linking"
-$MAKE CFLAGS="-Werror" USE_SHARED_LIB=1 check
-if ! ldd fsverity | grep libfsverity.so; then
-	fail "fsverity binary should be dynamically linked to libfsverity when USE_SHARED_LIB=1"
-fi
+	log "Check that all global symbols are prefixed with \"libfsverity_\""
+	if nm libfsverity.a | grep ' T ' | grep -v " libfsverity_"; then
+		fail "Some global symbols are not prefixed with \"libfsverity_\""
+	fi
+}
+TEST_FUNCS+=(static_linking_test)
 
-log "Check that all exported symbols are prefixed with \"libfsverity_\""
-if nm libfsverity.so | grep ' T ' | grep -v " libfsverity_"; then
-	fail "Some exported symbols are not prefixed with \"libfsverity_\""
-fi
+dynamic_linking_test()
+{
+	log "Build and test with dynamic linking"
+	$MAKE CFLAGS="-Werror" USE_SHARED_LIB=1 check
+	if ! ldd fsverity | grep libfsverity.so; then
+		fail "fsverity binary should be dynamically linked to libfsverity when USE_SHARED_LIB=1"
+	fi
 
-log "Test using libfsverity from C++ program"
-cat > "$TMPDIR/test.cc" <<EOF
+	log "Check that all exported symbols are prefixed with \"libfsverity_\""
+	if nm libfsverity.so | grep ' T ' | grep -v " libfsverity_"; then
+		fail "Some exported symbols are not prefixed with \"libfsverity_\""
+	fi
+}
+TEST_FUNCS+=(dynamic_linking_test)
+
+cplusplus_test()
+{
+	$MAKE CFLAGS="-Werror" libfsverity.so
+	log "Test using libfsverity from C++ program"
+	cat > "$TMPDIR/test.cc" <<EOF
 #include <libfsverity.h>
 #include <iostream>
 int main()
@@ -70,127 +85,231 @@
 	std::cout << libfsverity_get_digest_size(FS_VERITY_HASH_ALG_SHA256) << std::endl;
 }
 EOF
-c++ -Wall -Werror "$TMPDIR/test.cc" -Iinclude -L. -lfsverity -o "$TMPDIR/test"
-[ "$(LD_LIBRARY_PATH=. "$TMPDIR/test")" = "32" ]
-rm "${TMPDIR:?}"/*
-
-log "Check that build doesn't produce untracked files"
-$MAKE CFLAGS="-Werror" all test_programs
-if git status --short | grep -q '^??'; then
-	git status
-	fail "Build produced untracked files (check 'git status').  Missing gitignore entry?"
-fi
-
-log "Test that 'make uninstall' uninstalls all files"
-make DESTDIR="$TMPDIR" install
-if [ "$(find "$TMPDIR" -type f -o -type l | wc -l)" = 0 ]; then
-	fail "'make install' didn't install any files"
-fi
-make DESTDIR="$TMPDIR" uninstall
-if [ "$(find "$TMPDIR" -type f -o -type l | wc -l)" != 0 ]; then
-	fail "'make uninstall' didn't uninstall all files"
-fi
-rm -r "${TMPDIR:?}"/*
-
-log "Build, install, and uninstall with dash"
-make clean SHELL=/bin/dash
-make DESTDIR="$TMPDIR" SHELL=/bin/dash install
-make DESTDIR="$TMPDIR" SHELL=/bin/dash uninstall
-
-log "Check that all files have license and copyright info"
-list="$TMPDIR/filelist"
-filter_license_info() {
-	# files to exclude from license and copyright info checks
-	grep -E -v '(\.gitignore|LICENSE|.*\.md|testdata|fsverity_uapi\.h|libfsverity\.pc\.in)'
+	c++ -Wall -Werror "$TMPDIR/test.cc" -Iinclude -L. -lfsverity -o "$TMPDIR/test"
+	[ "$(LD_LIBRARY_PATH=. "$TMPDIR/test")" = "32" ]
+	rm "${TMPDIR:?}"/*
 }
-git grep -L 'SPDX-License-Identifier: MIT' \
-	| filter_license_info > "$list" || true
-if [ -s "$list" ]; then
-	fail "The following files are missing an appropriate SPDX license identifier: $(<"$list")"
+TEST_FUNCS+=(cplusplus_test)
+
+untracked_files_test()
+{
+	log "Check that build doesn't produce untracked files"
+	$MAKE CFLAGS="-Werror" all test_programs
+	if git status --short | grep -q '^??'; then
+		git status
+		fail "Build produced untracked files (check 'git status').  Missing gitignore entry?"
+	fi
+}
+TEST_FUNCS+=(untracked_files_test)
+
+uninstall_test()
+{
+	log "Test that 'make uninstall' uninstalls all files"
+	make DESTDIR="$TMPDIR" install
+	if [ "$(find "$TMPDIR" -type f -o -type l | wc -l)" = 0 ]; then
+		fail "'make install' didn't install any files"
+	fi
+	make DESTDIR="$TMPDIR" uninstall
+	if [ "$(find "$TMPDIR" -type f -o -type l | wc -l)" != 0 ]; then
+		fail "'make uninstall' didn't uninstall all files"
+	fi
+	rm -r "${TMPDIR:?}"/*
+}
+TEST_FUNCS+=(uninstall_test)
+
+dash_test()
+{
+	log "Build, install, and uninstall with dash"
+	make clean SHELL=/bin/dash
+	make DESTDIR="$TMPDIR" SHELL=/bin/dash install
+	make DESTDIR="$TMPDIR" SHELL=/bin/dash uninstall
+}
+TEST_FUNCS+=(dash_test)
+
+license_test()
+{
+	log "Check that all files have license and copyright info"
+	list="$TMPDIR/filelist"
+	filter_license_info() {
+		# files to exclude from license and copyright info checks
+		grep -E -v '(\.gitignore|LICENSE|.*\.md|testdata|fsverity_uapi\.h|libfsverity\.pc\.in)'
+	}
+	git grep -L 'SPDX-License-Identifier: MIT' \
+		| filter_license_info > "$list" || true
+	if [ -s "$list" ]; then
+		fail "The following files are missing an appropriate SPDX license identifier: $(<"$list")"
+	fi
+	# For now some people still prefer a free-form license statement, not just SPDX.
+	git grep -L 'Use of this source code is governed by an MIT-style' \
+		| filter_license_info > "$list" || true
+	if [ -s "$list" ]; then
+		fail "The following files are missing an appropriate license statement: $(<"$list")"
+	fi
+	git grep -L '\<Copyright\>' | filter_license_info > "$list" || true
+	if [ -s "$list" ]; then
+		fail "The following files are missing a copyright statement: $(<"$list")"
+	fi
+	rm "$list"
+}
+TEST_FUNCS+=(license_test)
+
+gcc_test()
+{
+	log "Build and test with gcc (-O2)"
+	$MAKE CC=gcc CFLAGS="-O2 -Werror" check
+
+	log "Build and test with gcc (-O3)"
+	$MAKE CC=gcc CFLAGS="-O3 -Werror" check
+}
+TEST_FUNCS+=(gcc_test)
+
+clang_test()
+{
+	log "Build and test with clang (-O2)"
+	$MAKE CC=clang CFLAGS="-O2 -Werror" check
+
+	log "Build and test with clang (-O3)"
+	$MAKE CC=clang CFLAGS="-O3 -Werror" check
+}
+TEST_FUNCS+=(clang_test)
+
+32bit_test()
+{
+	log "Build and test with gcc (32-bit)"
+	$MAKE CC=gcc CFLAGS="-O2 -Werror -m32" check
+}
+TEST_FUNCS+=(32bit_test)
+
+sanitizers_test()
+{
+	log "Build and test with clang + UBSAN"
+	$MAKE CC=clang \
+		CFLAGS="-O2 -Werror -fsanitize=undefined -fno-sanitize-recover=undefined" \
+		check
+
+	log "Build and test with clang + ASAN"
+	$MAKE CC=clang \
+		CFLAGS="-O2 -Werror -fsanitize=address -fno-sanitize-recover=address" \
+		check
+
+	log "Build and test with clang + unsigned integer overflow sanitizer"
+	$MAKE CC=clang \
+		CFLAGS="-O2 -Werror -fsanitize=unsigned-integer-overflow -fno-sanitize-recover=unsigned-integer-overflow" \
+		check
+
+	log "Build and test with clang + CFI"
+	$MAKE CC=clang CFLAGS="-O2 -Werror -fsanitize=cfi -flto -fvisibility=hidden" \
+		AR=llvm-ar check
+}
+TEST_FUNCS+=(sanitizers_test)
+
+valgrind_test()
+{
+	log "Build and test with valgrind"
+	$MAKE TEST_WRAPPER_PROG="valgrind --quiet --error-exitcode=100 --leak-check=full --errors-for-leak-kinds=all" \
+		CFLAGS="-O2 -Werror" check
+}
+TEST_FUNCS+=(valgrind_test)
+
+boringssl_test()
+{
+	log "Build and test using BoringSSL instead of OpenSSL"
+	log "-> Building BoringSSL"
+	$MAKE boringssl
+	log "-> Building fsverity-utils linked to BoringSSL"
+	$MAKE CFLAGS="-O2 -Werror" LDFLAGS="-Lboringssl/build/crypto" \
+		CPPFLAGS="-Iboringssl/include" LDLIBS="-lcrypto -lpthread" check
+}
+TEST_FUNCS+=(boringssl_test)
+
+openssl1_test()
+{
+	log "Build and test using OpenSSL 1.0"
+	$MAKE CFLAGS="-O2 -Werror" LDFLAGS="-L/usr/lib/openssl-1.0" \
+		CPPFLAGS="-I/usr/include/openssl-1.0" check
+}
+TEST_FUNCS+=(openssl1_test)
+
+openssl3_test()
+{
+	log "Build and test using OpenSSL 3.0"
+	OSSL3=$HOME/src/openssl/inst/usr/local
+	LD_LIBRARY_PATH="$OSSL3/lib64" $MAKE CFLAGS="-O2 -Werror" \
+		LDFLAGS="-L$OSSL3/lib64" CPPFLAGS="-I$OSSL3/include" check
+}
+TEST_FUNCS+=(openssl3_test)
+
+unsigned_char_test()
+{
+	log "Build and test using -funsigned-char"
+	$MAKE CFLAGS="-O2 -Werror -funsigned-char" check
+}
+TEST_FUNCS+=(unsigned_char_test)
+
+signed_char_test()
+{
+	log "Build and test using -fsigned-char"
+	$MAKE CFLAGS="-O2 -Werror -fsigned-char" check
+}
+TEST_FUNCS+=(signed_char_test)
+
+windows_build_test()
+{
+	log "Cross-compile for Windows (32-bit)"
+	$MAKE CC=i686-w64-mingw32-gcc CFLAGS="-O2 -Werror"
+
+	log "Cross-compile for Windows (64-bit)"
+	$MAKE CC=x86_64-w64-mingw32-gcc CFLAGS="-O2 -Werror"
+}
+TEST_FUNCS+=(windows_build_test)
+
+sparse_test()
+{
+	log "Run sparse"
+	./scripts/run-sparse.sh
+}
+TEST_FUNCS+=(sparse_test)
+
+clang_analyzer_test()
+{
+	log "Run clang static analyzer"
+	scan-build --status-bugs make CFLAGS="-O2 -Werror" all test_programs
+}
+TEST_FUNCS+=(clang_analyzer_test)
+
+shellcheck_test()
+{
+	log "Run shellcheck"
+	shellcheck scripts/*.sh 1>&2
+}
+TEST_FUNCS+=(shellcheck_test)
+
+test_exists()
+{
+	local tst=$1
+	local func
+	for func in "${TEST_FUNCS[@]}"; do
+		if [ "${tst}_test" = "$func" ]; then
+			return 0
+		fi
+	done
+	return 1
+}
+
+if [[ $# == 0 ]]; then
+	for func in "${TEST_FUNCS[@]}"; do
+		eval "$func"
+	done
+else
+	for tst; do
+		if ! test_exists "$tst"; then
+			echo 1>&2 "Unknown test: $tst"
+			exit 2
+		fi
+	done
+	for tst; do
+		eval "${tst}_test"
+	done
 fi
-# For now some people still prefer a free-form license statement, not just SPDX.
-git grep -L 'Use of this source code is governed by an MIT-style' \
-	| filter_license_info > "$list" || true
-if [ -s "$list" ]; then
-	fail "The following files are missing an appropriate license statement: $(<"$list")"
-fi
-git grep -L '\<Copyright\>' | filter_license_info > "$list" || true
-if [ -s "$list" ]; then
-	fail "The following files are missing a copyright statement: $(<"$list")"
-fi
-rm "$list"
-
-log "Build and test with gcc (-O2)"
-$MAKE CC=gcc CFLAGS="-O2 -Werror" check
-
-log "Build and test with gcc (-O3)"
-$MAKE CC=gcc CFLAGS="-O3 -Werror" check
-
-log "Build and test with gcc (32-bit)"
-$MAKE CC=gcc CFLAGS="-O2 -Werror -m32" check
-
-log "Build and test with clang (-O2)"
-$MAKE CC=clang CFLAGS="-O2 -Werror" check
-
-log "Build and test with clang (-O3)"
-$MAKE CC=clang CFLAGS="-O3 -Werror" check
-
-log "Build and test with clang + UBSAN"
-$MAKE CC=clang \
-	CFLAGS="-O2 -Werror -fsanitize=undefined -fno-sanitize-recover=undefined" \
-	check
-
-log "Build and test with clang + ASAN"
-$MAKE CC=clang \
-	CFLAGS="-O2 -Werror -fsanitize=address -fno-sanitize-recover=address" \
-	check
-
-log "Build and test with clang + unsigned integer overflow sanitizer"
-$MAKE CC=clang \
-	CFLAGS="-O2 -Werror -fsanitize=unsigned-integer-overflow -fno-sanitize-recover=unsigned-integer-overflow" \
-	check
-
-log "Build and test with clang + CFI"
-$MAKE CC=clang CFLAGS="-O2 -Werror -fsanitize=cfi -flto -fvisibility=hidden" \
-	check
-
-log "Build and test with valgrind"
-$MAKE TEST_WRAPPER_PROG="valgrind --quiet --error-exitcode=100 --leak-check=full --errors-for-leak-kinds=all" \
-	CFLAGS="-O2 -Werror" check
-
-log "Build and test using BoringSSL instead of OpenSSL"
-BSSL=$HOME/src/boringssl
-$MAKE CFLAGS="-O2 -Werror" LDFLAGS="-L$BSSL/build/crypto" \
-	CPPFLAGS="-I$BSSL/include" LDLIBS="-lcrypto -lpthread" check
-
-log "Build and test using OpenSSL 1.0"
-$MAKE CFLAGS="-O2 -Werror" LDFLAGS="-L/usr/lib/openssl-1.0" \
-	CPPFLAGS="-I/usr/include/openssl-1.0" check
-
-log "Build and test using OpenSSL 3.0"
-OSSL3=$HOME/src/openssl/inst/usr/local
-LD_LIBRARY_PATH="$OSSL3/lib64" $MAKE CFLAGS="-O2 -Werror" \
-	LDFLAGS="-L$OSSL3/lib64" CPPFLAGS="-I$OSSL3/include" check
-
-log "Build and test using -funsigned-char"
-$MAKE CFLAGS="-O2 -Werror -funsigned-char" check
-
-log "Build and test using -fsigned-char"
-$MAKE CFLAGS="-O2 -Werror -fsigned-char" check
-
-log "Cross-compile for Windows (32-bit)"
-$MAKE CC=i686-w64-mingw32-gcc CFLAGS="-O2 -Werror"
-
-log "Cross-compile for Windows (64-bit)"
-$MAKE CC=x86_64-w64-mingw32-gcc CFLAGS="-O2 -Werror"
-
-log "Run sparse"
-./scripts/run-sparse.sh
-
-log "Run clang static analyzer"
-scan-build --status-bugs make CFLAGS="-O2 -Werror" all test_programs
-
-log "Run shellcheck"
-shellcheck scripts/*.sh 1>&2
 
 log "All tests passed!"