diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-05-10 06:55:39 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-05-10 06:55:39 +0000 |
commit | a94b62680ad9bcaeee71186fa4be3d2af12f51eb (patch) | |
tree | bf153cc41ae7aeecb590993a582eca8ca07f988b | |
parent | 9af7e8dc4f84dddba7864da972db300e510e5281 (diff) | |
parent | 84ff393b112706a82c491b8c8e6062635b503670 (diff) | |
download | fsverity-utils-android13-mainline-sdkext-release.tar.gz |
Snap for 8564071 from 84ff393b112706a82c491b8c8e6062635b503670 to mainline-sdkext-releaseaml_sdk_331812000aml_sdk_331811100aml_sdk_331811000aml_sdk_331412000aml_sdk_331410000aml_sdk_331310010aml_sdk_331111000aml_sdk_330810050aml_sdk_330810010android13-mainline-sdkext-release
Change-Id: I6d4014c36afaa9f4f80fcf7ed7cee79e3eaae793
-rw-r--r-- | .github/workflows/ci.yml | 169 | ||||
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | Android.bp | 6 | ||||
-rw-r--r-- | METADATA | 10 | ||||
-rw-r--r-- | Makefile | 56 | ||||
-rw-r--r-- | NEWS.md | 22 | ||||
-rw-r--r-- | README.md | 66 | ||||
-rw-r--r-- | common/fsverity_uapi.h | 14 | ||||
-rw-r--r-- | include/libfsverity.h | 106 | ||||
-rw-r--r-- | lib/compute_digest.c | 130 | ||||
-rw-r--r-- | lib/lib_private.h | 5 | ||||
-rw-r--r-- | lib/libfsverity.pc.in | 2 | ||||
-rw-r--r-- | lib/sign_digest.c | 96 | ||||
-rw-r--r-- | lib/utils.c | 34 | ||||
-rw-r--r-- | man/fsverity.1.md | 215 | ||||
-rw-r--r-- | programs/cmd_digest.c | 7 | ||||
-rw-r--r-- | programs/cmd_dump_metadata.c | 163 | ||||
-rw-r--r-- | programs/cmd_sign.c | 57 | ||||
-rw-r--r-- | programs/fsverity.c | 101 | ||||
-rw-r--r-- | programs/fsverity.h | 13 | ||||
-rw-r--r-- | programs/test_compute_digest.c | 133 | ||||
-rw-r--r-- | programs/utils.c | 59 | ||||
-rw-r--r-- | programs/utils.h | 3 | ||||
-rwxr-xr-x | scripts/do-release.sh | 95 | ||||
-rwxr-xr-x | scripts/run-sparse.sh | 2 | ||||
-rwxr-xr-x | scripts/run-tests.sh | 364 |
26 files changed, 1655 insertions, 277 deletions
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 @@ -1,9 +1,13 @@ +*.[1-9] *.a +*.exe *.o *.patch *.so *.so.* /.build-config +/boringssl +/boringssl.tar.gz /fsverity /fsverity.sig /run-tests.log @@ -63,6 +63,12 @@ cc_library { export_include_dirs: ["include"], + apex_available: [ + "//apex_available:platform", + "com.android.compos", + ], + recovery_available: true, + srcs: [ "lib/*.c", ], @@ -5,12 +5,12 @@ third_party { type: GIT value: "https://git.kernel.org/pub/scm/linux/kernel/git/ebiggers/fsverity-utils.git" } - version: "v1.3" - # would be NOTICE save for common/fsverity_uapi.h + version: "20e87c13075a8e5660a8d69fd6c93d4f7c5f01a5" + license_note: "would be NOTICE save for common/fsverity_uapi.h" license_type: RESTRICTED last_upgrade_date { - year: 2021 - month: 1 - day: 19 + year: 2022 + month: 2 + day: 7 } } @@ -25,6 +25,9 @@ # Define LIBDIR to override where to install libraries, like './configure # --libdir' in autotools-based projects (default: PREFIX/lib) # +# Define MANDIR to override where to install man pages, like './configure +# --mandir' in autotools-based projects (default: PREFIX/share/man) +# # Define DESTDIR to override the installation destination directory # (default: empty string) # @@ -41,8 +44,13 @@ ifneq ($(findstring -mingw,$(shell $(CC) -dumpmachine 2>/dev/null)),) MINGW = 1 endif +# 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-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. CFLAGS ?= -O2 - override CFLAGS := -Wall -Wundef \ $(call cc-option,-Wdeclaration-after-statement) \ $(call cc-option,-Wimplicit-fallthrough) \ @@ -51,6 +59,7 @@ override CFLAGS := -Wall -Wundef \ $(call cc-option,-Wstrict-prototypes) \ $(call cc-option,-Wunused-parameter) \ $(call cc-option,-Wvla) \ + $(call cc-option,-Wno-deprecated-declarations) \ $(CFLAGS) override CPPFLAGS := -Iinclude -D_FILE_OFFSET_BITS=64 -D_GNU_SOURCE $(CPPFLAGS) @@ -61,12 +70,14 @@ QUIET_CCLD = @echo ' CCLD ' $@; QUIET_AR = @echo ' AR ' $@; QUIET_LN = @echo ' LN ' $@; QUIET_GEN = @echo ' GEN ' $@; +QUIET_PANDOC = @echo ' PANDOC ' $@; endif USE_SHARED_LIB ?= PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/bin INCDIR ?= $(PREFIX)/include LIBDIR ?= $(PREFIX)/lib +MANDIR ?= $(PREFIX)/share/man DESTDIR ?= ifneq ($(MINGW),1) PKGCONF ?= pkg-config @@ -92,6 +103,7 @@ FSVERITY := fsverity$(EXEEXT) fi DEFAULT_TARGETS := +EXTRA_TARGETS := COMMON_HEADERS := $(wildcard common/*.h) LDLIBS := $(shell "$(PKGCONF)" libcrypto --libs 2>/dev/null || echo -lcrypto) CFLAGS += $(shell "$(PKGCONF)" libcrypto --cflags 2>/dev/null || echo) @@ -160,6 +172,7 @@ FSVERITY_PROG_OBJ := $(PROG_COMMON_OBJ) \ programs/fsverity.o ifneq ($(MINGW),1) FSVERITY_PROG_OBJ += \ + programs/cmd_dump_metadata.o \ programs/cmd_enable.o \ programs/cmd_measure.o endif @@ -186,9 +199,37 @@ DEFAULT_TARGETS += $(FSVERITY) $(TEST_PROGRAMS): %$(EXEEXT): programs/%.o $(PROG_COMMON_OBJ) libfsverity.a $(QUIET_CCLD) $(CC) -o $@ $+ $(CFLAGS) $(LDFLAGS) $(LDLIBS) +EXTRA_TARGETS += $(TEST_PROGRAMS) + ############################################################################## -SPECIAL_TARGETS := all test_programs check install uninstall help clean +#### Manual pages + +man/fsverity.1:man/fsverity.1.md + $(QUIET_PANDOC) pandoc $+ -s -t man > $@ + +MAN_PAGES := man/fsverity.1 +EXTRA_TARGETS += $(MAN_PAGES) + +############################################################################## + +# 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 FORCE: @@ -232,6 +273,10 @@ install:all > $(DESTDIR)$(LIBDIR)/pkgconfig/libfsverity.pc chmod 644 $(DESTDIR)$(LIBDIR)/pkgconfig/libfsverity.pc +install-man:$(MAN_PAGES) + install -d $(DESTDIR)$(MANDIR)/man1 + install -m644 $+ $(DESTDIR)$(MANDIR)/man1/ + uninstall: rm -f $(DESTDIR)$(BINDIR)/$(FSVERITY) rm -f $(DESTDIR)$(LIBDIR)/libfsverity.a @@ -239,15 +284,18 @@ uninstall: rm -f $(DESTDIR)$(LIBDIR)/libfsverity.so rm -f $(DESTDIR)$(LIBDIR)/pkgconfig/libfsverity.pc rm -f $(DESTDIR)$(INCDIR)/libfsverity.h + for page in $(notdir $(MAN_PAGES)); do \ + rm -f $(DESTDIR)$(MANDIR)/man1/$$page; \ + done help: @echo "Available targets:" @echo "------------------" - @for target in $(DEFAULT_TARGETS) $(TEST_PROGRAMS) $(SPECIAL_TARGETS); \ + @for target in $(DEFAULT_TARGETS) $(EXTRA_TARGETS) $(SPECIAL_TARGETS); \ do \ echo $$target; \ done clean: - rm -f $(DEFAULT_TARGETS) $(TEST_PROGRAMS) \ + rm -f $(DEFAULT_TARGETS) $(EXTRA_TARGETS) \ lib/*.o programs/*.o .build-config fsverity.sig @@ -1,5 +1,27 @@ # 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. + +* Added the `fsverity dump_metadata` subcommand. + +* Added the `--out-merkle-tree` and `--out-descriptor` options to + `fsverity digest` and `fsverity sign`. + +* Added metadata callbacks support to `libfsverity_compute_digest()`. + ## Version 1.3 * Added a `fsverity digest` subcommand. @@ -24,20 +24,22 @@ See `libfsverity.h` for the API of this library. ## Building and installing -fsverity-utils uses the OpenSSL library, so you first must install the -needed development files. For example, on Debian-based systems, run: +To build fsverity-utils, first install the needed build dependencies. For +example, on Debian-based systems, run: ```bash sudo apt-get install libssl-dev + sudo apt-get install pandoc # optional ``` -OpenSSL must be version 1.0.0 or later. +OpenSSL must be version 1.0.0 or later. This is the only runtime dependency. Then, to build and install fsverity-utils: ```bash make sudo make install + sudo make install-man # optional ``` By default, the following targets are built and installed: the program @@ -45,6 +47,9 @@ By default, the following targets are built and installed: the program `libfsverity.so`. You can also run `make check` to build and run the tests, or `make help` to display all available build targets. +`make install-man` installs the `fsverity.1` manual page. This step requires +that `pandoc` be installed. + By default, `fsverity` is statically linked to `libfsverity`. You can use `make USE_SHARED_LIB=1` to use dynamic linking instead. @@ -63,6 +68,9 @@ A Windows build of OpenSSL/libcrypto needs to be available. ## Examples +Full usage information for `fsverity` can be found in the manual page +(`man fsverity`). Here, we just show some typical examples. + ### Basic use ```bash @@ -94,11 +102,44 @@ against a trusted value. ### 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: @@ -129,15 +170,6 @@ keyring. Here's an example: 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/common/fsverity_uapi.h b/common/fsverity_uapi.h index a739c9a..c59a897 100644 --- a/common/fsverity_uapi.h +++ b/common/fsverity_uapi.h @@ -85,7 +85,21 @@ struct fsverity_formatted_digest { __u8 digest[]; }; +#define FS_VERITY_METADATA_TYPE_MERKLE_TREE 1 +#define FS_VERITY_METADATA_TYPE_DESCRIPTOR 2 +#define FS_VERITY_METADATA_TYPE_SIGNATURE 3 + +struct fsverity_read_metadata_arg { + __u64 metadata_type; + __u64 offset; + __u64 length; + __u64 buf_ptr; + __u64 __reserved; +}; + #define FS_IOC_ENABLE_VERITY _IOW('f', 133, struct fsverity_enable_arg) #define FS_IOC_MEASURE_VERITY _IOWR('f', 134, struct fsverity_digest) +#define FS_IOC_READ_VERITY_METADATA \ + _IOWR('f', 135, struct fsverity_read_metadata_arg) #endif /* _UAPI_LINUX_FSVERITY_H */ diff --git a/include/libfsverity.h b/include/libfsverity.h index 6c42e5e..a0a1527 100644 --- a/include/libfsverity.h +++ b/include/libfsverity.h @@ -22,7 +22,7 @@ extern "C" { #include <stdint.h> #define FSVERITY_UTILS_MAJOR_VERSION 1 -#define FSVERITY_UTILS_MINOR_VERSION 3 +#define FSVERITY_UTILS_MINOR_VERSION 5 #define FS_VERITY_HASH_ALG_SHA256 1 #define FS_VERITY_HASH_ALG_SHA512 2 @@ -61,8 +61,18 @@ struct libfsverity_merkle_tree_params { /** @reserved1: must be 0 */ uint64_t reserved1[8]; + /** + * @metadata_callbacks: if non-NULL, this gives a set of callback + * functions to which libfsverity_compute_digest() will pass the Merkle + * tree blocks and fs-verity descriptor after they are computed. + * Normally this isn't useful, but this can be needed in rare cases + * where the metadata needs to be consumed by something other than one + * of the native Linux kernel implementations of fs-verity. + */ + const struct libfsverity_metadata_callbacks *metadata_callbacks; + /** @reserved2: must be 0 */ - uintptr_t reserved2[8]; + uintptr_t reserved2[7]; }; struct libfsverity_digest { @@ -71,11 +81,75 @@ struct libfsverity_digest { uint8_t digest[]; /* the actual digest */ }; +/** + * struct libfsverity_signature_params - certificate and private key information + * + * Zero this, then set @certfile. Then, to specify the private key by key file, + * set @keyfile. Alternatively, to specify the private key by PKCS#11 token, + * set @pkcs11_engine, @pkcs11_module, and optionally @pkcs11_keyid. + * + * Support for PKCS#11 tokens is unavailable when libfsverity was linked to + * BoringSSL rather than OpenSSL. + */ struct libfsverity_signature_params { - const char *keyfile; /* path to key file (PEM format) */ - const char *certfile; /* path to certificate (PEM format) */ - uint64_t reserved1[8]; /* must be 0 */ - uintptr_t reserved2[8]; /* must be 0 */ + + /** @keyfile: the path to the key file in PEM format, when applicable */ + const char *keyfile; + + /** @certfile: the path to the certificate file in PEM format */ + const char *certfile; + + /** @reserved1: must be 0 */ + uint64_t reserved1[8]; + + /** + * @pkcs11_engine: the path to the PKCS#11 engine .so file, when + * applicable + */ + const char *pkcs11_engine; + + /** + * @pkcs11_module: the path to the PKCS#11 module .so file, when + * applicable + */ + const char *pkcs11_module; + + /** @pkcs11_keyid: the PKCS#11 key identifier, when applicable */ + const char *pkcs11_keyid; + + /** @reserved2: must be 0 */ + uintptr_t reserved2[5]; +}; + +struct libfsverity_metadata_callbacks { + + /** @ctx: context passed to the below callbacks (opaque to library) */ + void *ctx; + + /** + * @merkle_tree_size: if non-NULL, called with the total size of the + * Merkle tree in bytes, prior to any call to @merkle_tree_block. Must + * return 0 on success, or a negative errno value on failure. + */ + int (*merkle_tree_size)(void *ctx, uint64_t size); + + /** + * @merkle_tree_block: if non-NULL, called with each block of the + * Merkle tree after it is computed. The offset is the offset in bytes + * to the block within the Merkle tree, using the Merkle tree layout + * used by FS_IOC_READ_VERITY_METADATA. The offsets won't necessarily + * be in increasing order. Must return 0 on success, or a negative + * errno value on failure. + */ + int (*merkle_tree_block)(void *ctx, const void *block, size_t size, + uint64_t offset); + + /** + * @descriptor: if non-NULL, called with the fs-verity descriptor after + * it is computed. Must return 0 on success, or a negative errno value + * on failure. + */ + int (*descriptor)(void *ctx, const void *descriptor, size_t size); }; /* @@ -101,7 +175,8 @@ typedef int (*libfsverity_read_fn_t)(void *fd, void *buf, size_t count); * * Returns: * * 0 for success, -EINVAL for invalid input arguments, -ENOMEM if libfsverity - * failed to allocate memory, or an error returned by @read_fn. + * failed to allocate memory, or an error returned by @read_fn or by one of + * the @params->metadata_callbacks. * * digest_ret returns a pointer to the digest on success. The digest object * is allocated by libfsverity and must be freed by the caller using free(). */ @@ -111,16 +186,15 @@ libfsverity_compute_digest(void *fd, libfsverity_read_fn_t read_fn, 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: struct libfsverity_signature_params providing filenames of - * the keyfile and certificate file. Reserved fields must be zero. + * @sig_params: pointer to the certificate and private key information * @sig_ret: Pointer to pointer for signed digest * @sig_size_ret: Pointer to size of signed return digest * diff --git a/lib/compute_digest.c b/lib/compute_digest.c index a4f649c..c5b0100 100644 --- a/lib/compute_digest.c +++ b/lib/compute_digest.c @@ -24,9 +24,8 @@ struct block_buffer { /* * Hash a block, writing the result to the next level's pending block buffer. - * Returns true if the next level's block became full, else false. */ -static bool hash_one_block(struct hash_ctx *hash, struct block_buffer *cur, +static void hash_one_block(struct hash_ctx *hash, struct block_buffer *cur, u32 block_size, const u8 *salt, u32 salt_size) { struct block_buffer *next = cur + 1; @@ -41,8 +40,60 @@ static bool hash_one_block(struct hash_ctx *hash, struct block_buffer *cur, next->filled += hash->alg->digest_size; cur->filled = 0; +} + +static bool block_is_full(const struct block_buffer *block, u32 block_size, + struct hash_ctx *hash) +{ + /* Would the next hash put us over the limit? */ + return block->filled + hash->alg->digest_size > block_size; +} + +static int report_merkle_tree_size(const struct libfsverity_metadata_callbacks *cbs, + u64 size) +{ + if (cbs && cbs->merkle_tree_size) { + int err = cbs->merkle_tree_size(cbs->ctx, size); - return next->filled + hash->alg->digest_size > block_size; + if (err) { + libfsverity_error_msg("error processing Merkle tree size"); + return err; + } + } + return 0; +} + +static int report_merkle_tree_block(const struct libfsverity_metadata_callbacks *cbs, + const struct block_buffer *block, + u32 block_size, u64 *level_offset) +{ + + if (cbs && cbs->merkle_tree_block) { + int err = cbs->merkle_tree_block(cbs->ctx, block->data, + block_size, + *level_offset * block_size); + + if (err) { + libfsverity_error_msg("error processing Merkle tree block"); + return err; + } + (*level_offset)++; + } + return 0; +} + +static int report_descriptor(const struct libfsverity_metadata_callbacks *cbs, + const void *descriptor, size_t size) +{ + if (cbs && cbs->descriptor) { + int err = cbs->descriptor(cbs->ctx, descriptor, size); + + if (err) { + libfsverity_error_msg("error processing fs-verity descriptor"); + return err; + } + } + return 0; } /* @@ -52,6 +103,7 @@ static bool hash_one_block(struct hash_ctx *hash, struct block_buffer *cur, static int compute_root_hash(void *fd, libfsverity_read_fn_t read_fn, u64 file_size, struct hash_ctx *hash, u32 block_size, const u8 *salt, u32 salt_size, + const struct libfsverity_metadata_callbacks *metadata_cbs, u8 *root_hash) { const u32 hashes_per_block = block_size / hash->alg->digest_size; @@ -60,6 +112,7 @@ static int compute_root_hash(void *fd, libfsverity_read_fn_t read_fn, u64 blocks; int num_levels = 0; int level; + u64 level_offset[FS_VERITY_MAX_LEVELS]; struct block_buffer _buffers[1 + FS_VERITY_MAX_LEVELS + 1] = {}; struct block_buffer *buffers = &_buffers[1]; u64 offset; @@ -68,7 +121,7 @@ static int compute_root_hash(void *fd, libfsverity_read_fn_t read_fn, /* Root hash of empty file is all 0's */ if (file_size == 0) { memset(root_hash, 0, hash->alg->digest_size); - return 0; + return report_merkle_tree_size(metadata_cbs, 0); } if (salt_size != 0) { @@ -78,15 +131,39 @@ static int compute_root_hash(void *fd, libfsverity_read_fn_t read_fn, memcpy(padded_salt, salt, salt_size); } - /* Compute number of levels */ - for (blocks = DIV_ROUND_UP(file_size, block_size); blocks > 1; - blocks = DIV_ROUND_UP(blocks, hashes_per_block)) { + /* Compute number of levels and the number of blocks in each level. */ + blocks = DIV_ROUND_UP(file_size, block_size); + while (blocks > 1) { if (WARN_ON(num_levels >= FS_VERITY_MAX_LEVELS)) { err = -EINVAL; goto out; } - num_levels++; + blocks = DIV_ROUND_UP(blocks, hashes_per_block); + /* + * Temporarily use level_offset[] to store the number of blocks + * in each level. It will be overwritten later. + */ + level_offset[num_levels++] = blocks; + } + + /* + * Compute the starting block of each level, using the convention where + * the root level is first, i.e. the convention used by + * FS_IOC_READ_VERITY_METADATA. At the same time, compute the total + * size of the Merkle tree. These values are only needed for the + * metadata callbacks (if they were given), as the hash computation + * itself doesn't prescribe an ordering of the levels and doesn't + * prescribe any special meaning to the total size of the Merkle tree. + */ + offset = 0; + for (level = num_levels - 1; level >= 0; level--) { + blocks = level_offset[level]; + level_offset[level] = offset; + offset += blocks; } + err = report_merkle_tree_size(metadata_cbs, offset * block_size); + if (err) + goto out; /* * Allocate the block buffers. Buffer "-1" is for data blocks. @@ -112,21 +189,33 @@ static int compute_root_hash(void *fd, libfsverity_read_fn_t read_fn, goto out; } - level = -1; - while (hash_one_block(hash, &buffers[level], block_size, - padded_salt, padded_salt_size)) { - level++; - if (WARN_ON(level >= num_levels)) { - err = -EINVAL; + hash_one_block(hash, &buffers[-1], block_size, + padded_salt, padded_salt_size); + for (level = 0; level < num_levels; level++) { + if (!block_is_full(&buffers[level], block_size, hash)) + break; + hash_one_block(hash, &buffers[level], block_size, + padded_salt, padded_salt_size); + err = report_merkle_tree_block(metadata_cbs, + &buffers[level], + block_size, + &level_offset[level]); + if (err) goto out; - } } } /* Finish all nonempty pending tree blocks */ for (level = 0; level < num_levels; level++) { - if (buffers[level].filled != 0) + if (buffers[level].filled != 0) { hash_one_block(hash, &buffers[level], block_size, padded_salt, padded_salt_size); + err = report_merkle_tree_block(metadata_cbs, + &buffers[level], + block_size, + &level_offset[level]); + if (err) + goto out; + } } /* Root hash was filled by the last call to hash_one_block() */ @@ -217,8 +306,13 @@ libfsverity_compute_digest(void *fd, libfsverity_read_fn_t read_fn, } err = compute_root_hash(fd, read_fn, params->file_size, hash, - block_size, params->salt, - params->salt_size, desc.root_hash); + block_size, params->salt, params->salt_size, + params->metadata_callbacks, desc.root_hash); + if (err) + goto out; + + err = report_descriptor(params->metadata_callbacks, + &desc, sizeof(desc)); if (err) goto out; diff --git a/lib/lib_private.h b/lib/lib_private.h index 7768eea..8532636 100644 --- a/lib/lib_private.h +++ b/lib/lib_private.h @@ -58,14 +58,11 @@ void *libfsverity_zalloc(size_t size); void *libfsverity_memdup(const void *mem, size_t size); __cold void -libfsverity_do_error_msg(const char *format, va_list va, int err); +libfsverity_do_error_msg(const char *format, va_list va); __printf(1, 2) __cold void libfsverity_error_msg(const char *format, ...); -__printf(1, 2) __cold void -libfsverity_error_msg_errno(const char *format, ...); - __cold void libfsverity_warn_on(const char *condition, const char *file, int line); diff --git a/lib/libfsverity.pc.in b/lib/libfsverity.pc.in index be3ef44..4c9bb20 100644 --- a/lib/libfsverity.pc.in +++ b/lib/libfsverity.pc.in @@ -4,7 +4,7 @@ includedir=@INCDIR@ Name: libfsverity Description: fs-verity library -Version: 1.3 +Version: 1.5 Libs: -L${libdir} -lfsverity Requires.private: libcrypto Cflags: -I${includedir} diff --git a/lib/sign_digest.c b/lib/sign_digest.c index 9a35256..d726772 100644 --- a/lib/sign_digest.c +++ b/lib/sign_digest.c @@ -19,6 +19,10 @@ #include <openssl/pkcs7.h> #include <string.h> +#ifndef OPENSSL_IS_BORINGSSL +#include <openssl/engine.h> +#endif + static int print_openssl_err_cb(const char *str, size_t len __attribute__((unused)), void *u __attribute__((unused))) @@ -34,7 +38,7 @@ error_msg_openssl(const char *format, ...) va_list va; va_start(va, format); - libfsverity_do_error_msg(format, va, 0); + libfsverity_do_error_msg(format, va); va_end(va); if (ERR_peek_error() == 0) @@ -81,6 +85,11 @@ static int read_certificate(const char *certfile, X509 **cert_ret) X509 *cert; int err; + if (!certfile) { + libfsverity_error_msg("no certificate specified"); + return -EINVAL; + } + errno = 0; bio = BIO_new_file(certfile, "r"); if (!bio) { @@ -212,6 +221,15 @@ out: return err; } +static int +load_pkcs11_private_key(const struct libfsverity_signature_params *sig_params + __attribute__((unused)), + EVP_PKEY **pkey_ret __attribute__((unused))) +{ + libfsverity_error_msg("BoringSSL doesn't support PKCS#11 tokens"); + return -EINVAL; +} + #else /* OPENSSL_IS_BORINGSSL */ static BIO *new_mem_buf(const void *buf, size_t size) @@ -315,16 +333,79 @@ out: return err; } +static int +load_pkcs11_private_key(const struct libfsverity_signature_params *sig_params, + EVP_PKEY **pkey_ret) +{ + ENGINE *engine; + + if (!sig_params->pkcs11_engine) { + libfsverity_error_msg("no PKCS#11 engine specified"); + return -EINVAL; + } + if (!sig_params->pkcs11_module) { + libfsverity_error_msg("no PKCS#11 module specified"); + return -EINVAL; + } + ENGINE_load_dynamic(); + engine = ENGINE_by_id("dynamic"); + if (!engine) { + error_msg_openssl("failed to initialize OpenSSL PKCS#11 engine"); + return -EINVAL; + } + if (!ENGINE_ctrl_cmd_string(engine, "SO_PATH", + sig_params->pkcs11_engine, 0) || + !ENGINE_ctrl_cmd_string(engine, "ID", "pkcs11", 0) || + !ENGINE_ctrl_cmd_string(engine, "LIST_ADD", "1", 0) || + !ENGINE_ctrl_cmd_string(engine, "LOAD", NULL, 0) || + !ENGINE_ctrl_cmd_string(engine, "MODULE_PATH", + sig_params->pkcs11_module, 0) || + !ENGINE_init(engine)) { + error_msg_openssl("failed to initialize OpenSSL PKCS#11 engine"); + ENGINE_free(engine); + return -EINVAL; + } + *pkey_ret = ENGINE_load_private_key(engine, sig_params->pkcs11_keyid, + NULL, NULL); + ENGINE_finish(engine); + ENGINE_free(engine); + if (!*pkey_ret) { + error_msg_openssl("failed to load private key from PKCS#11 token"); + return -EINVAL; + } + return 0; +} + #endif /* !OPENSSL_IS_BORINGSSL */ +/* Get a private key, either from disk or from a PKCS#11 token. */ +static int +get_private_key(const struct libfsverity_signature_params *sig_params, + EVP_PKEY **pkey_ret) +{ + if (sig_params->pkcs11_engine || sig_params->pkcs11_module || + sig_params->pkcs11_keyid) { + if (sig_params->keyfile) { + libfsverity_error_msg("private key must be specified either by file or by PKCS#11 token, not both"); + return -EINVAL; + } + return load_pkcs11_private_key(sig_params, pkey_ret); + } + if (!sig_params->keyfile) { + libfsverity_error_msg("no private key specified"); + return -EINVAL; + } + return read_private_key(sig_params->keyfile, pkey_ret); +} + LIBEXPORT int libfsverity_sign_digest(const struct libfsverity_digest *digest, const struct libfsverity_signature_params *sig_params, u8 **sig_ret, size_t *sig_size_ret) { const struct fsverity_hash_alg *hash_alg; - EVP_PKEY *pkey = NULL; X509 *cert = NULL; + EVP_PKEY *pkey = NULL; const EVP_MD *md; struct fsverity_formatted_digest *d = NULL; int err; @@ -334,11 +415,6 @@ libfsverity_sign_digest(const struct libfsverity_digest *digest, return -EINVAL; } - if (!sig_params->keyfile || !sig_params->certfile) { - libfsverity_error_msg("keyfile and certfile must be specified"); - return -EINVAL; - } - if (!libfsverity_mem_is_zeroed(sig_params->reserved1, sizeof(sig_params->reserved1)) || !libfsverity_mem_is_zeroed(sig_params->reserved2, @@ -353,11 +429,11 @@ libfsverity_sign_digest(const struct libfsverity_digest *digest, return -EINVAL; } - err = read_private_key(sig_params->keyfile, &pkey); + err = read_certificate(sig_params->certfile, &cert); if (err) goto out; - err = read_certificate(sig_params->certfile, &cert); + err = get_private_key(sig_params, &pkey); if (err) goto out; @@ -383,8 +459,8 @@ libfsverity_sign_digest(const struct libfsverity_digest *digest, err = sign_pkcs7(d, sizeof(*d) + digest->digest_size, pkey, cert, md, sig_ret, sig_size_ret); out: - EVP_PKEY_free(pkey); X509_free(cert); + EVP_PKEY_free(pkey); free(d); return err; } diff --git a/lib/utils.c b/lib/utils.c index 036dd60..d506ef1 100644 --- a/lib/utils.c +++ b/lib/utils.c @@ -51,16 +51,7 @@ libfsverity_set_error_callback(void (*cb)(const char *msg)) libfsverity_error_cb = cb; } -#ifdef _WIN32 -static char *strerror_r(int errnum, char *buf, size_t buflen) -{ - strerror_s(buf, buflen, errnum); - - return buf; -} -#endif - -void libfsverity_do_error_msg(const char *format, va_list va, int err) +void libfsverity_do_error_msg(const char *format, va_list va) { int saved_errno = errno; char *msg = NULL; @@ -71,18 +62,8 @@ void libfsverity_do_error_msg(const char *format, va_list va, int err) if (vasprintf(&msg, format, va) < 0) goto out; - if (err) { - char *msg2 = NULL; - char errbuf[64]; - - if (asprintf(&msg2, "%s: %s", msg, - strerror_r(err, errbuf, sizeof(errbuf))) < 0) - goto out2; - free(msg); - msg = msg2; - } (*libfsverity_error_cb)(msg); -out2: + free(msg); out: errno = saved_errno; @@ -93,16 +74,7 @@ void libfsverity_error_msg(const char *format, ...) va_list va; va_start(va, format); - libfsverity_do_error_msg(format, va, 0); - va_end(va); -} - -void libfsverity_error_msg_errno(const char *format, ...) -{ - va_list va; - - va_start(va, format); - libfsverity_do_error_msg(format, va, errno); + libfsverity_do_error_msg(format, va); va_end(va); } diff --git a/man/fsverity.1.md b/man/fsverity.1.md new file mode 100644 index 0000000..dd54964 --- /dev/null +++ b/man/fsverity.1.md @@ -0,0 +1,215 @@ +% FSVERITY(1) fsverity-utils v1.5 | User Commands +% +% February 2022 + +# NAME + +fsverity - userspace utility for fs-verity + +# SYNOPSIS +**fsverity digest** [*OPTION*...] *FILE*... \ +**fsverity dump_metadata** [*OPTION*...] *TYPE* *FILE* \ +**fsverity enable** [*OPTION*...] *FILE* \ +**fsverity measure** *FILE*... \ +**fsverity sign** [*OPTION*...] *FILE* *OUT_SIGFILE* + +# DESCRIPTION + +**fsverity** is a userspace utility for fs-verity. fs-verity is a Linux kernel +filesystem feature that does transparent on-demand verification of the contents +of read-only files using Merkle trees. + +**fsverity** can enable fs-verity on files, retrieve the digests of fs-verity +files, and sign files for use with fs-verity (among other things). +**fsverity**'s functionality is divided among various subcommands. + +This manual page focuses on documenting all **fsverity** subcommands and +options. For examples and more information about the fs-verity kernel feature, +see the references at the end of this page. + +# OPTIONS + +**fsverity** always accepts the following options: + +**\-\-help** +: Show the help, for either one subcommand or for all subcommands. + +**\-\-version** +: Show the version of fsverity-utils. + +# SUBCOMMANDS + +## **fsverity digest** [*OPTION*...] *FILE*... + +Compute the fs-verity digest of the given file(s). This is mainly intended to +used in preparation for signing the digest. In some cases **fsverity sign** +can be used instead to digest and sign the file in one step. + +Options accepted by **fsverity digest**: + +**\-\-block-size**=*BLOCK_SIZE* +: The Merkle tree block size (in bytes) to use. This must be a power of 2 and + at least twice the size of the hash values. However, note that currently + (as of Linux kernel v5.13), the Linux kernel implementations of fs-verity + only support the case where the Merkle tree block size is equal to the + system page size, usually 4096 bytes. The default value of this option is + 4096. + +**\-\-compact** +: When printing the file digest, only print the actual digest hex string; + don't print the algorithm name and filename. + +**\-\-for-builtin-sig** +: Format the file digest in a way that is compatible with the Linux kernel's + fs-verity built-in signature verification support. This means formatting it + as a `struct fsverity_formatted_digest`. Use this option if you are using + built-in signatures but are not using **fsverity sign** to do the signing. + +**\-\-hash-alg**=*HASH_ALG* +: The hash algorithm to use to build the Merkle tree. Valid options are + sha256 and sha512. Default is sha256. + +**\-\-out-merkle-tree**=*FILE* +: Write the computed Merkle tree to the given file. The Merkle tree layout + will be the same as that used by the Linux kernel's + `FS_IOC_READ_VERITY_METADATA` ioctl. + + Normally this option isn't useful, but it can be needed in cases where the + fs-verity metadata needs to be consumed by something other than one of the + native Linux kernel implementations of fs-verity. This is not needed for + file signing. + +**\-\-out-descriptor**=*FILE* +: Write the computed fs-verity descriptor to the given file. + + Normally this option isn't useful, but it can be needed in cases where the + fs-verity metadata needs to be consumed by something other than one of the + native Linux kernel implementations of fs-verity. This is not needed for + file signing. + +**\-\-salt**=*SALT* +: The salt to use in the Merkle tree, as a hex string. The salt is a value + that is prepended to every hashed block; it can be used to personalize the + hashing for a particular file or device. The default is no salt. + +## **fsverity dump_metadata** [*OPTION*...] *TYPE* *FILE* + +Dump the fs-verity metadata of the given file. The file must have fs-verity +enabled, and the filesystem must support the `FS_IOC_READ_VERITY_METADATA` ioctl +(it was added in Linux v5.12). This subcommand normally isn't useful, but it +can be useful in cases where a userspace server program is serving a verity file +to a client which implements fs-verity compatible verification. + +*TYPE* may be "merkle\_tree", "descriptor", or "signature", indicating the type +of metadata to dump. "signature" refers to the built-in signature, if present; +userspace-managed signatures will not be included. + +Options accepted by **fsverity dump_metadata**: + +**\-\-length**=*LENGTH* +: Length in bytes to dump from the specified metadata item. Only accepted in + combination with **\-\-offset**. + +**\-\-offset**=*offset* +: Offset in bytes into the specified metadata item at which to start dumping. + Only accepted in combination with **\-\-length**. + +## **fsverity enable** [*OPTION*...] *FILE* + +Enable fs-verity on the specified file. This will only work if the filesystem +supports fs-verity. + +Options accepted by **fsverity enable**: + +**\-\-block-size**=*BLOCK_SIZE* +: Same as for **fsverity digest**. + +**\-\-hash-alg**=*HASH_ALG* +: Same as for **fsverity digest**. + +**\-\-salt**=*SALT* +: Same as for **fsverity digest**. + +**\-\-signature**=*SIGFILE* +: Specifies the built-in signature to apply to the file. *SIGFILE* must be a + file that contains the signature in PKCS#7 DER format, e.g. as produced by + the **fsverity sign** command. + + Note that this option is only needed if the Linux kernel's fs-verity + built-in signature verification support is being used. It is not needed if + the signatures will be verified in userspace, as in that case the signatures + should be stored separately. + +## **fsverity measure** *FILE*... + +Display the fs-verity digest of the given file(s). The files must have +fs-verity enabled. The output will be the same as **fsverity digest** with +the appropriate parameters, but **fsverity measure** will take constant time +for each file regardless of the size of the file. + +**fsverity measure** does not accept any options. + +## **fsverity sign** [*OPTION*...] *FILE* *OUT_SIGFILE* + +Sign the given file for fs-verity, in a way that is compatible with the Linux +kernel's fs-verity built-in signature verification support. The signature will +be written to *OUT_SIGFILE* in PKCS#7 DER format. + +The private key can be specified either by key file or by PKCS#11 token. To use +a key file, provide **\-\-key** and optionally **\-\-cert**. To use a PKCS#11 +token, provide **\-\-pkcs11-engine**, **\-\-pkcs11-module**, **\-\-cert**, and +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* +: Same as for **fsverity digest**. + +**\-\-cert**=*CERTFILE* +: Specifies the file that contains the certificate, in PEM format. This + option is required if *KEYFILE* contains only the private key and not also + the certificate, or if a PKCS#11 token is used. + +**\-\-hash-alg**=*HASH_ALG* +: Same as for **fsverity digest**. + +**\-\-key**=*KEYFILE* +: Specifies the file that contains the private key, in PEM format. This + option is required when not using a PKCS#11 token. + +**\-\-out-descriptor**=*FILE* +: Same as for **fsverity digest**. + +**\-\-out-merkle-tree**=*FILE* +: Same as for **fsverity digest**. + +**\-\-pkcs11-engine**=*SOFILE* +: Specifies the path to the OpenSSL PKCS#11 engine file. This typically will + be a path to the libp11 .so file. This option is required when using a + PKCS#11 token. + +**\-\-pkcs11-keyid**=*KEYID* +: Specifies the key identifier in the form of a PKCS#11 URI. If not provided, + the default key associated with the token is used. This option is only + applicable when using a PKCS#11 token. + +**\-\-pkcs11-module**=*SOFILE* +: Specifies the path to the PKCS#11 token-specific module library. This + option is required when using a PKCS#11 token. + +**\-\-salt**=*SALT* +: Same as for **fsverity digest**. + +# SEE ALSO + +For example commands and more information, see the +[README file for +fsverity-utils](https://git.kernel.org/pub/scm/linux/kernel/git/ebiggers/fsverity-utils.git/tree/README.md). + +Also see the [kernel documentation for +fs-verity](https://www.kernel.org/doc/html/latest/filesystems/fsverity.html). diff --git a/programs/cmd_digest.c b/programs/cmd_digest.c index 1a3c769..fd9f4de 100644 --- a/programs/cmd_digest.c +++ b/programs/cmd_digest.c @@ -18,6 +18,8 @@ static const struct option longopts[] = { {"hash-alg", required_argument, NULL, OPT_HASH_ALG}, {"block-size", required_argument, NULL, OPT_BLOCK_SIZE}, {"salt", required_argument, NULL, OPT_SALT}, + {"out-merkle-tree", required_argument, NULL, OPT_OUT_MERKLE_TREE}, + {"out-descriptor", required_argument, NULL, OPT_OUT_DESCRIPTOR}, {"compact", no_argument, NULL, OPT_COMPACT}, {"for-builtin-sig", no_argument, NULL, OPT_FOR_BUILTIN_SIG}, {NULL, 0, NULL, 0} @@ -40,6 +42,8 @@ int fsverity_cmd_digest(const struct fsverity_command *cmd, case OPT_HASH_ALG: case OPT_BLOCK_SIZE: case OPT_SALT: + case OPT_OUT_MERKLE_TREE: + case OPT_OUT_DESCRIPTOR: if (!parse_tree_param(c, optarg, &tree_params)) goto out_usage; break; @@ -114,7 +118,8 @@ int fsverity_cmd_digest(const struct fsverity_command *cmd, } status = 0; out: - destroy_tree_params(&tree_params); + if (!destroy_tree_params(&tree_params) && status == 0) + status = 1; return status; out_err: diff --git a/programs/cmd_dump_metadata.c b/programs/cmd_dump_metadata.c new file mode 100644 index 0000000..94cc8ec --- /dev/null +++ b/programs/cmd_dump_metadata.c @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +/* + * The 'fsverity dump_metadata' command + * + * 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. + */ + +#include "fsverity.h" + +#include <fcntl.h> +#include <getopt.h> +#include <sys/ioctl.h> +#include <unistd.h> + +static const struct option longopts[] = { + {"offset", required_argument, NULL, OPT_OFFSET}, + {"length", required_argument, NULL, OPT_LENGTH}, + {NULL, 0, NULL, 0} +}; + +static const struct { + const char *name; + int val; +} metadata_types[] = { + {"merkle_tree", FS_VERITY_METADATA_TYPE_MERKLE_TREE}, + {"descriptor", FS_VERITY_METADATA_TYPE_DESCRIPTOR}, + {"signature", FS_VERITY_METADATA_TYPE_SIGNATURE}, +}; + +static bool parse_metadata_type(const char *name, __u64 *val_ret) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(metadata_types); i++) { + if (strcmp(name, metadata_types[i].name) == 0) { + *val_ret = metadata_types[i].val; + return true; + } + } + error_msg("unknown metadata type: %s", name); + fputs(" Expected", stderr); + for (i = 0; i < ARRAY_SIZE(metadata_types); i++) { + if (i != 0 && ARRAY_SIZE(metadata_types) > 2) + putc(',', stderr); + putc(' ', stderr); + if (i != 0 && i == ARRAY_SIZE(metadata_types) - 1) + fputs("or ", stderr); + fprintf(stderr, "\"%s\"", metadata_types[i].name); + } + fprintf(stderr, "\n"); + return false; +} + +/* Dump the fs-verity metadata of the given file. */ +int fsverity_cmd_dump_metadata(const struct fsverity_command *cmd, + int argc, char *argv[]) +{ + bool offset_specified = false; + bool length_specified = false; + struct filedes file = { .fd = -1 }; + struct filedes stdout_filedes = { .fd = STDOUT_FILENO, + .name = "stdout" }; + struct fsverity_read_metadata_arg arg = { .length = 32768 }; + void *buf = NULL; + char *tmp; + int c; + int status; + int bytes_read; + + while ((c = getopt_long(argc, argv, "", longopts, NULL)) != -1) { + switch (c) { + case OPT_OFFSET: + if (offset_specified) { + error_msg("--offset can only be specified once"); + goto out_usage; + } + errno = 0; + arg.offset = strtoull(optarg, &tmp, 10); + if (errno || *tmp) { + error_msg("invalid value for --offset"); + goto out_usage; + } + offset_specified = true; + break; + case OPT_LENGTH: + if (length_specified) { + error_msg("--length can only be specified once"); + goto out_usage; + } + errno = 0; + arg.length = strtoull(optarg, &tmp, 10); + if (errno || *tmp || arg.length > SIZE_MAX) { + error_msg("invalid value for --length"); + goto out_usage; + } + length_specified = true; + break; + default: + goto out_usage; + } + } + + argv += optind; + argc -= optind; + + if (argc != 2) + goto out_usage; + + if (!parse_metadata_type(argv[0], &arg.metadata_type)) + goto out_usage; + + if (length_specified && !offset_specified) { + error_msg("--length specified without --offset"); + goto out_usage; + } + if (offset_specified && !length_specified) { + error_msg("--offset specified without --length"); + goto out_usage; + } + + buf = xzalloc(arg.length); + arg.buf_ptr = (uintptr_t)buf; + + if (!open_file(&file, argv[1], O_RDONLY, 0)) + goto out_err; + + /* + * If --offset and --length were specified, then do only the single read + * requested. Otherwise read until EOF. + */ + do { + bytes_read = ioctl(file.fd, FS_IOC_READ_VERITY_METADATA, &arg); + if (bytes_read < 0) { + error_msg_errno("FS_IOC_READ_VERITY_METADATA failed on '%s'", + file.name); + goto out_err; + } + if (bytes_read == 0) + break; + if (!full_write(&stdout_filedes, buf, bytes_read)) + goto out_err; + arg.offset += bytes_read; + } while (!length_specified); + + status = 0; +out: + free(buf); + filedes_close(&file); + return status; + +out_err: + status = 1; + goto out; + +out_usage: + usage(cmd, stderr); + status = 2; + goto out; +} diff --git a/programs/cmd_sign.c b/programs/cmd_sign.c index 47ba6a2..aab8f00 100644 --- a/programs/cmd_sign.c +++ b/programs/cmd_sign.c @@ -27,11 +27,16 @@ static bool write_signature(const char *filename, const u8 *sig, u32 sig_size) } static const struct option longopts[] = { - {"hash-alg", required_argument, NULL, OPT_HASH_ALG}, - {"block-size", required_argument, NULL, OPT_BLOCK_SIZE}, - {"salt", required_argument, NULL, OPT_SALT}, - {"key", required_argument, NULL, OPT_KEY}, - {"cert", required_argument, NULL, OPT_CERT}, + {"key", required_argument, NULL, OPT_KEY}, + {"cert", required_argument, NULL, OPT_CERT}, + {"pkcs11-engine", required_argument, NULL, OPT_PKCS11_ENGINE}, + {"pkcs11-module", required_argument, NULL, OPT_PKCS11_MODULE}, + {"pkcs11-keyid", required_argument, NULL, OPT_PKCS11_KEYID}, + {"hash-alg", required_argument, NULL, OPT_HASH_ALG}, + {"block-size", required_argument, NULL, OPT_BLOCK_SIZE}, + {"salt", required_argument, NULL, OPT_SALT}, + {"out-merkle-tree", required_argument, NULL, OPT_OUT_MERKLE_TREE}, + {"out-descriptor", required_argument, NULL, OPT_OUT_DESCRIPTOR}, {NULL, 0, NULL, 0} }; @@ -51,12 +56,6 @@ int fsverity_cmd_sign(const struct fsverity_command *cmd, while ((c = getopt_long(argc, argv, "", longopts, NULL)) != -1) { switch (c) { - case OPT_HASH_ALG: - case OPT_BLOCK_SIZE: - case OPT_SALT: - if (!parse_tree_param(c, optarg, &tree_params)) - goto out_usage; - break; case OPT_KEY: if (sig_params.keyfile != NULL) { error_msg("--key can only be specified once"); @@ -71,6 +70,35 @@ int fsverity_cmd_sign(const struct fsverity_command *cmd, } sig_params.certfile = optarg; break; + case OPT_PKCS11_ENGINE: + if (sig_params.pkcs11_engine != NULL) { + error_msg("--pkcs11-engine can only be specified once"); + goto out_usage; + } + sig_params.pkcs11_engine = optarg; + break; + case OPT_PKCS11_MODULE: + if (sig_params.pkcs11_module != NULL) { + error_msg("--pkcs11-module can only be specified once"); + goto out_usage; + } + sig_params.pkcs11_module = optarg; + break; + case OPT_PKCS11_KEYID: + if (sig_params.pkcs11_keyid != NULL) { + error_msg("--pkcs11-keyid can only be specified once"); + goto out_usage; + } + sig_params.pkcs11_keyid = optarg; + break; + case OPT_HASH_ALG: + case OPT_BLOCK_SIZE: + case OPT_SALT: + case OPT_OUT_MERKLE_TREE: + case OPT_OUT_DESCRIPTOR: + if (!parse_tree_param(c, optarg, &tree_params)) + goto out_usage; + break; default: goto out_usage; } @@ -82,10 +110,6 @@ int fsverity_cmd_sign(const struct fsverity_command *cmd, if (argc != 2) goto out_usage; - if (sig_params.keyfile == NULL) { - error_msg("Missing --key argument"); - goto out_usage; - } if (sig_params.certfile == NULL) sig_params.certfile = sig_params.keyfile; @@ -117,7 +141,8 @@ int fsverity_cmd_sign(const struct fsverity_command *cmd, status = 0; out: filedes_close(&file); - destroy_tree_params(&tree_params); + if (!destroy_tree_params(&tree_params) && status == 0) + status = 1; free(digest); free(sig); return status; diff --git a/programs/fsverity.c b/programs/fsverity.c index b911b2e..e4e348b 100644 --- a/programs/fsverity.c +++ b/programs/fsverity.c @@ -11,6 +11,7 @@ #include "fsverity.h" +#include <fcntl.h> #include <limits.h> static const struct fsverity_command { @@ -27,9 +28,16 @@ static const struct fsverity_command { .usage_str = " fsverity digest FILE...\n" " [--hash-alg=HASH_ALG] [--block-size=BLOCK_SIZE] [--salt=SALT]\n" +" [--out-merkle-tree=FILE] [--out-descriptor=FILE]\n" " [--compact] [--for-builtin-sig]\n" #ifndef _WIN32 }, { + .name = "dump_metadata", + .func = fsverity_cmd_dump_metadata, + .short_desc = "Dump the fs-verity metadata of the given file", + .usage_str = +" fsverity dump_metadata TYPE FILE [--offset=OFFSET] [--length=LENGTH]\n" + }, { .name = "enable", .func = fsverity_cmd_enable, .short_desc = "Enable fs-verity on a file", @@ -48,11 +56,13 @@ static const struct fsverity_command { }, { .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 --key=KEYFILE\n" +" fsverity sign FILE OUT_SIGFILE\n" +" [--key=KEYFILE] [--cert=CERTFILE] [--pkcs11-engine=SOFILE]\n" +" [--pkcs11-module=SOFILE] [--pkcs11-keyid=KEYID]\n" " [--hash-alg=HASH_ALG] [--block-size=BLOCK_SIZE] [--salt=SALT]\n" -" [--cert=CERTFILE]\n" +" [--out-merkle-tree=FILE] [--out-descriptor=FILE]\n" } }; @@ -194,6 +204,74 @@ static bool parse_salt_option(const char *arg, u8 **salt_ptr, return true; } +struct metadata_callback_ctx { + struct filedes merkle_tree_file; + struct filedes descriptor_file; + struct libfsverity_metadata_callbacks callbacks; +}; + +static int handle_merkle_tree_size(void *_ctx, u64 size) +{ + struct metadata_callback_ctx *ctx = _ctx; + + if (!preallocate_file(&ctx->merkle_tree_file, size)) + return -EIO; + return 0; +} + +static int handle_merkle_tree_block(void *_ctx, const void *block, size_t size, + u64 offset) +{ + struct metadata_callback_ctx *ctx = _ctx; + + if (!full_pwrite(&ctx->merkle_tree_file, block, size, offset)) + return -EIO; + return 0; +} + +static int handle_descriptor(void *_ctx, const void *descriptor, size_t size) +{ + struct metadata_callback_ctx *ctx = _ctx; + + if (!full_write(&ctx->descriptor_file, descriptor, size)) + return -EIO; + return 0; +} + +static bool parse_out_metadata_option(int opt_char, const char *arg, + const struct libfsverity_metadata_callbacks **cbs) +{ + struct metadata_callback_ctx *ctx; + struct filedes *file; + const char *opt_name; + + if (*cbs) { + ctx = (*cbs)->ctx; + } else { + ctx = xzalloc(sizeof(*ctx)); + ctx->merkle_tree_file.fd = -1; + ctx->descriptor_file.fd = -1; + ctx->callbacks.ctx = ctx; + *cbs = &ctx->callbacks; + } + + if (opt_char == OPT_OUT_MERKLE_TREE) { + file = &ctx->merkle_tree_file; + opt_name = "--out-merkle-tree"; + ctx->callbacks.merkle_tree_size = handle_merkle_tree_size; + ctx->callbacks.merkle_tree_block = handle_merkle_tree_block; + } else { + file = &ctx->descriptor_file; + opt_name = "--out-descriptor"; + ctx->callbacks.descriptor = handle_descriptor; + } + if (file->fd >= 0) { + error_msg("%s can only be specified once", opt_name); + return false; + } + return open_file(file, arg, O_WRONLY|O_CREAT|O_TRUNC, 0644); +} + bool parse_tree_param(int opt_char, const char *arg, struct libfsverity_merkle_tree_params *params) { @@ -205,15 +283,30 @@ bool parse_tree_param(int opt_char, const char *arg, case OPT_SALT: return parse_salt_option(arg, (u8 **)¶ms->salt, ¶ms->salt_size); + case OPT_OUT_MERKLE_TREE: + case OPT_OUT_DESCRIPTOR: + return parse_out_metadata_option(opt_char, arg, + ¶ms->metadata_callbacks); default: ASSERT(0); } } -void destroy_tree_params(struct libfsverity_merkle_tree_params *params) +bool destroy_tree_params(struct libfsverity_merkle_tree_params *params) { + bool ok = true; + free((u8 *)params->salt); + if (params->metadata_callbacks) { + struct metadata_callback_ctx *ctx = + params->metadata_callbacks->ctx; + + ok &= filedes_close(&ctx->merkle_tree_file); + ok &= filedes_close(&ctx->descriptor_file); + free(ctx); + } memset(params, 0, sizeof(*params)); + return ok; } int main(int argc, char *argv[]) diff --git a/programs/fsverity.h b/programs/fsverity.h index 45c4fe1..ad54cc2 100644 --- a/programs/fsverity.h +++ b/programs/fsverity.h @@ -27,6 +27,13 @@ enum { OPT_FOR_BUILTIN_SIG, OPT_HASH_ALG, OPT_KEY, + OPT_LENGTH, + OPT_OFFSET, + OPT_OUT_DESCRIPTOR, + OPT_OUT_MERKLE_TREE, + OPT_PKCS11_ENGINE, + OPT_PKCS11_KEYID, + OPT_PKCS11_MODULE, OPT_SALT, OPT_SIGNATURE, }; @@ -37,6 +44,10 @@ struct fsverity_command; int fsverity_cmd_digest(const struct fsverity_command *cmd, int argc, char *argv[]); +/* cmd_dump_metadata.c */ +int fsverity_cmd_dump_metadata(const struct fsverity_command *cmd, + int argc, char *argv[]); + /* cmd_enable.c */ int fsverity_cmd_enable(const struct fsverity_command *cmd, int argc, char *argv[]); @@ -53,6 +64,6 @@ int fsverity_cmd_sign(const struct fsverity_command *cmd, void usage(const struct fsverity_command *cmd, FILE *fp); bool parse_tree_param(int opt_char, const char *arg, struct libfsverity_merkle_tree_params *params); -void destroy_tree_params(struct libfsverity_merkle_tree_params *params); +bool destroy_tree_params(struct libfsverity_merkle_tree_params *params); #endif /* PROGRAMS_FSVERITY_H */ diff --git a/programs/test_compute_digest.c b/programs/test_compute_digest.c index e7f2645..67266fa 100644 --- a/programs/test_compute_digest.c +++ b/programs/test_compute_digest.c @@ -13,6 +13,7 @@ #include <ctype.h> #include <inttypes.h> +#include <openssl/sha.h> struct mem_file { u8 *data; @@ -37,6 +38,13 @@ static int error_read_fn(void *fd __attribute__((unused)), return -EIO; } +static int zeroes_read_fn(void *fd __attribute__((unused)), + void *buf, size_t count) +{ + memset(buf, 0, count); + return 0; +} + static const struct test_case { u32 hash_algorithm; u32 block_size; @@ -249,6 +257,130 @@ static void test_invalid_params(void) ASSERT(d == NULL); } +static struct { + u64 merkle_tree_size; + u64 merkle_tree_block; + u64 descriptor; +} metadata_callback_counts; + +static int handle_merkle_tree_size(void *ctx, u64 size) +{ + metadata_callback_counts.merkle_tree_size++; + + /* Test that the ctx argument is passed through correctly. */ + ASSERT(ctx == (void *)1); + + /* Test that the expected Merkle tree size is reported. */ + ASSERT(size == 5 * 1024); + return 0; +} + +static int handle_merkle_tree_block(void *ctx, const void *block, size_t size, + u64 offset) +{ + u8 digest[SHA256_DIGEST_LENGTH]; + u64 count = metadata_callback_counts.merkle_tree_block++; + const char *expected_digest; + + /* Test that ->merkle_tree_size() was called first. */ + ASSERT(metadata_callback_counts.merkle_tree_size == 1); + + /* Test that the ctx argument is passed through correctly. */ + ASSERT(ctx == (void *)1); + + /* + * Test that this Merkle tree block has the expected size, offset, and + * contents. The 4 blocks at "level 0" should be reported first, in + * order; then the 1 block at "level 1" should be reported last (but the + * level 1 block should have the smallest offset). + */ + ASSERT(size == 1024); + SHA256(block, size, digest); + if (count == 4) { + /* 1 block at level 1 */ + ASSERT(offset == 0); + expected_digest = "\x68\xc5\x38\xe1\x19\x58\xd6\x5d" + "\x68\xb6\xfe\x8e\x9f\xb8\xcc\xab" + "\xec\xfd\x92\x8b\x01\xd0\x63\x44" + "\xe2\x23\xed\x41\xdd\xc4\x54\x4a"; + } else { + /* 4 blocks at level 0 */ + ASSERT(offset == 1024 + (count * 1024)); + if (count < 3) { + expected_digest = "\xf7\x89\xba\xab\x53\x85\x9f\xaf" + "\x36\xd6\xd7\x5d\x10\x42\x06\x42" + "\x94\x20\x2d\x6e\x13\xe7\x71\x6f" + "\x39\x4f\xba\x43\x4c\xcc\x49\x86"; + } else { + expected_digest = "\x00\xfe\xd0\x3c\x5d\x6e\xab\x21" + "\x31\x43\xf3\xd9\x6a\x5c\xa3\x1c" + "\x2b\x89\xf5\x68\x4e\x6c\x8e\x07" + "\x87\x3e\x5e\x97\x65\x17\xb4\x8f"; + } + } + ASSERT(!memcmp(digest, expected_digest, SHA256_DIGEST_LENGTH)); + return 0; +} + +static const u8 expected_file_digest[SHA256_DIGEST_LENGTH] = + "\x09\xcb\xba\xee\xd2\xa0\x4c\x2d\xa2\x42\xc1\x0e\x15\x68\xd9\x6f" + "\x35\x8a\x16\xaa\x1e\xbe\x8c\xf0\x28\x61\x20\xc1\x3c\x93\x66\xd1"; + +static int handle_descriptor(void *ctx, const void *descriptor, size_t size) +{ + u8 digest[SHA256_DIGEST_LENGTH]; + + metadata_callback_counts.descriptor++; + /* Test that the ctx argument is passed through correctly. */ + ASSERT(ctx == (void *)1); + + /* Test that the fs-verity descriptor is reported correctly. */ + ASSERT(size == 256); + SHA256(descriptor, size, digest); + ASSERT(!memcmp(digest, expected_file_digest, SHA256_DIGEST_LENGTH)); + return 0; +} + +static const struct libfsverity_metadata_callbacks metadata_callbacks = { + .ctx = (void *)1, /* arbitrary value for testing purposes */ + .merkle_tree_size = handle_merkle_tree_size, + .merkle_tree_block = handle_merkle_tree_block, + .descriptor = handle_descriptor, +}; + +/* Test that the libfsverity_metadata_callbacks work correctly. */ +static void test_metadata_callbacks(void) +{ + /* + * For a useful test, we want a file whose Merkle tree will have at + * least 2 levels (this one will have exactly 2). The contents of the + * file aren't too important. + */ + struct libfsverity_merkle_tree_params params = { + .version = 1, + .hash_algorithm = FS_VERITY_HASH_ALG_SHA256, + .block_size = 1024, + .file_size = 100000, + .metadata_callbacks = &metadata_callbacks, + }; + struct libfsverity_digest *d; + + ASSERT(libfsverity_compute_digest(NULL, zeroes_read_fn, + ¶ms, &d) == 0); + + /* Test that the callbacks were called the correct number of times. */ + ASSERT(metadata_callback_counts.merkle_tree_size == 1); + ASSERT(metadata_callback_counts.merkle_tree_block == 5); + ASSERT(metadata_callback_counts.descriptor == 1); + + /* Test that the computed file digest is as expected. */ + ASSERT(d->digest_algorithm == FS_VERITY_HASH_ALG_SHA256); + ASSERT(d->digest_size == SHA256_DIGEST_LENGTH); + ASSERT(!memcmp(d->digest, expected_file_digest, SHA256_DIGEST_LENGTH)); + + free(d); +} + int main(int argc, char *argv[]) { const bool update = (argc == 2 && !strcmp(argv[1], "--update")); @@ -305,6 +437,7 @@ int main(int argc, char *argv[]) } test_invalid_params(); + test_metadata_callbacks(); printf("test_compute_digest passed\n"); return 0; } diff --git a/programs/utils.c b/programs/utils.c index ce19b57..116eb95 100644 --- a/programs/utils.c +++ b/programs/utils.c @@ -13,10 +13,14 @@ #include <errno.h> #include <fcntl.h> +#include <inttypes.h> #include <limits.h> #include <stdarg.h> #include <sys/stat.h> #include <unistd.h> +#ifdef _WIN32 +# include <windows.h> +#endif /* ========== Memory allocation ========== */ @@ -126,6 +130,26 @@ bool get_file_size(struct filedes *file, u64 *size_ret) return true; } +bool preallocate_file(struct filedes *file, u64 size) +{ + int res; + + if (size == 0) + return true; +#ifdef _WIN32 + /* Not exactly the same as posix_fallocate(), but good enough... */ + res = _chsize_s(file->fd, size); +#else + res = posix_fallocate(file->fd, 0, size); +#endif + if (res != 0) { + error_msg_errno("preallocating %" PRIu64 "-byte file '%s'", + size, file->name); + return false; + } + return true; +} + bool full_read(struct filedes *file, void *buf, size_t count) { while (count) { @@ -160,6 +184,41 @@ bool full_write(struct filedes *file, const void *buf, size_t count) return true; } +static int raw_pwrite(int fd, const void *buf, int count, u64 offset) +{ +#ifdef _WIN32 + HANDLE h = (HANDLE)_get_osfhandle(fd); + OVERLAPPED pos = { .Offset = offset, .OffsetHigh = offset >> 32 }; + DWORD written = 0; + + /* Not exactly the same as pwrite(), but good enough... */ + if (!WriteFile(h, buf, count, &written, &pos)) { + errno = EIO; + return -1; + } + return written; +#else + return pwrite(fd, buf, count, offset); +#endif +} + +bool full_pwrite(struct filedes *file, const void *buf, size_t count, + u64 offset) +{ + while (count) { + int n = raw_pwrite(file->fd, buf, min(count, INT_MAX), offset); + + if (n < 0) { + error_msg_errno("writing to '%s'", file->name); + return false; + } + buf += n; + count -= n; + offset += n; + } + return true; +} + bool filedes_close(struct filedes *file) { int res; diff --git a/programs/utils.h b/programs/utils.h index ab5005f..9a5c97a 100644 --- a/programs/utils.h +++ b/programs/utils.h @@ -40,8 +40,11 @@ struct filedes { bool open_file(struct filedes *file, const char *filename, int flags, int mode); bool get_file_size(struct filedes *file, u64 *size_ret); +bool preallocate_file(struct filedes *file, u64 size); bool full_read(struct filedes *file, void *buf, size_t count); bool full_write(struct filedes *file, const void *buf, size_t count); +bool full_pwrite(struct filedes *file, const void *buf, size_t count, + u64 offset); bool filedes_close(struct filedes *file); int read_callback(void *file, void *buf, size_t count); diff --git a/scripts/do-release.sh b/scripts/do-release.sh index 352fcf1..3f68497 100755 --- a/scripts/do-release.sh +++ b/scripts/do-release.sh @@ -9,38 +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 - -major=$(echo "$VERS" | cut -d. -f1) -minor=$(echo "$VERS" | cut -d. -f2) -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 -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" - -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 +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) + + 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" +} + +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 530fe34..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 @@ exec 2> >(tee -ia run-tests.log 1>&2) 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 - -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=() -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 +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 "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 "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) -log "Test using libfsverity from C++ program" -cat > "$TMPDIR/test.cc" <<EOF +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,118 +85,231 @@ int main() 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 + c++ -Wall -Werror "$TMPDIR/test.cc" -Iinclude -L. -lfsverity -o "$TMPDIR/test" + [ "$(LD_LIBRARY_PATH=. "$TMPDIR/test")" = "32" ] + rm "${TMPDIR:?}"/* +} +TEST_FUNCS+=(cplusplus_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:?}"/* - -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|NEWS|README|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" +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 (-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) -log "Build and test with gcc (-O3)" -$MAKE CC=gcc CFLAGS="-O3 -Werror" check +clang_test() +{ + log "Build and test with clang (-O2)" + $MAKE CC=clang CFLAGS="-O2 -Werror" check -log "Build and test with gcc (32-bit)" -$MAKE CC=gcc CFLAGS="-O2 -Werror -m32" check + log "Build and test with clang (-O3)" + $MAKE CC=clang CFLAGS="-O3 -Werror" check +} +TEST_FUNCS+=(clang_test) -log "Build and test with clang (-O2)" -$MAKE CC=clang CFLAGS="-O2 -Werror" check +32bit_test() +{ + log "Build and test with gcc (32-bit)" + $MAKE CC=gcc CFLAGS="-O2 -Werror -m32" check +} +TEST_FUNCS+=(32bit_test) -log "Build and test with clang (-O3)" -$MAKE CC=clang CFLAGS="-O3 -Werror" check +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) -log "Build and test with clang + UBSAN" -$MAKE CC=clang \ - CFLAGS="-O2 -Werror -fsanitize=undefined -fno-sanitize-recover=undefined" \ - check +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) -log "Build and test with clang + ASAN" -$MAKE CC=clang \ - CFLAGS="-O2 -Werror -fsanitize=address -fno-sanitize-recover=address" \ - check +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) -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 +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) -log "Build and test with clang + CFI" -$MAKE CC=clang CFLAGS="-O2 -Werror -fsanitize=cfi -flto -fvisibility=hidden" \ - check +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) -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 +unsigned_char_test() +{ + log "Build and test using -funsigned-char" + $MAKE CFLAGS="-O2 -Werror -funsigned-char" check +} +TEST_FUNCS+=(unsigned_char_test) -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 +signed_char_test() +{ + log "Build and test using -fsigned-char" + $MAKE CFLAGS="-O2 -Werror -fsigned-char" check +} +TEST_FUNCS+=(signed_char_test) -log "Build and test using -funsigned-char" -$MAKE CFLAGS="-O2 -Werror -funsigned-char" check +windows_build_test() +{ + log "Cross-compile for Windows (32-bit)" + $MAKE CC=i686-w64-mingw32-gcc CFLAGS="-O2 -Werror" -log "Build and test using -fsigned-char" -$MAKE CFLAGS="-O2 -Werror -fsigned-char" check + log "Cross-compile for Windows (64-bit)" + $MAKE CC=x86_64-w64-mingw32-gcc CFLAGS="-O2 -Werror" +} +TEST_FUNCS+=(windows_build_test) -log "Cross-compile for Windows (32-bit)" -$MAKE CC=i686-w64-mingw32-gcc CFLAGS="-O2 -Werror" +sparse_test() +{ + log "Run sparse" + ./scripts/run-sparse.sh +} +TEST_FUNCS+=(sparse_test) -log "Cross-compile for Windows (64-bit)" -$MAKE CC=x86_64-w64-mingw32-gcc CFLAGS="-O2 -Werror" +clang_analyzer_test() +{ + log "Run clang static analyzer" + scan-build --status-bugs make CFLAGS="-O2 -Werror" all test_programs +} +TEST_FUNCS+=(clang_analyzer_test) -log "Run sparse" -./scripts/run-sparse.sh +shellcheck_test() +{ + log "Run shellcheck" + shellcheck scripts/*.sh 1>&2 +} +TEST_FUNCS+=(shellcheck_test) -log "Run clang static analyzer" -scan-build --status-bugs make CFLAGS="-O2 -Werror" all test_programs +test_exists() +{ + local tst=$1 + local func + for func in "${TEST_FUNCS[@]}"; do + if [ "${tst}_test" = "$func" ]; then + return 0 + fi + done + return 1 +} -log "Run shellcheck" -shellcheck scripts/*.sh 1>&2 +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 log "All tests passed!" |