diff --git a/libcomposefs/lcfs-internal.h b/libcomposefs/lcfs-internal.h index f0877031..05e33cd5 100644 --- a/libcomposefs/lcfs-internal.h +++ b/libcomposefs/lcfs-internal.h @@ -42,6 +42,9 @@ typedef int errint_t; */ #define LCFS_BUILD_INLINE_FILE_SIZE_LIMIT 64 +/* What may be returned by the kernel for digests */ +#define MAX_DIGEST_SIZE 64 + #define OVERLAY_XATTR_USER_PREFIX "user." #define OVERLAY_XATTR_TRUSTED_PREFIX "trusted." #define OVERLAY_XATTR_PARTIAL_PREFIX "overlay." diff --git a/libcomposefs/lcfs-mount.c b/libcomposefs/lcfs-mount.c index 5b1a0a86..eb0ed142 100644 --- a/libcomposefs/lcfs-mount.c +++ b/libcomposefs/lcfs-mount.c @@ -139,8 +139,6 @@ static int syscall_mount_setattr(int dfd, const char *path, unsigned int flags, } #endif -#define MAX_DIGEST_SIZE 64 - struct lcfs_mount_state_s { const char *image_path; const char *mountpoint; diff --git a/libcomposefs/lcfs-writer.c b/libcomposefs/lcfs-writer.c index 29292bea..f7e2ef88 100644 --- a/libcomposefs/lcfs-writer.c +++ b/libcomposefs/lcfs-writer.c @@ -22,14 +22,17 @@ #include "lcfs-writer.h" #include "lcfs-utils.h" #include "lcfs-fsverity.h" +#include "lcfs-mount.h" #include "lcfs-erofs.h" #include "hash.h" #include #include +#include #include #include #include +#include #include #include #include @@ -521,12 +524,53 @@ int lcfs_compute_fsverity_from_content(uint8_t *digest, void *file, lcfs_read_cb return 0; } +// Given a file descriptor, perform an in-memory computation of its fsverity +// digest. Note that the computation starts from the current offset position of +// the file. int lcfs_compute_fsverity_from_fd(uint8_t *digest, int fd) { int _fd = fd; return lcfs_compute_fsverity_from_content(digest, &_fd, fsverity_read_cb); } +// Given a file descriptor, first query the kernel for its fsverity digest. If +// it is not available in the kernel, perform an in-memory computation. The file +// position will always be reset to zero if needed. +int lcfs_fd_get_fsverity(uint8_t *digest, int fd) +{ + struct { + struct fsverity_digest fsv; + char buf[MAX_DIGEST_SIZE]; + } buf; + + // First, ask the kernel if the file already has fsverity; if so we just return + // that. + buf.fsv.digest_size = MAX_DIGEST_SIZE; + int res = ioctl(fd, FS_IOC_MEASURE_VERITY, &buf.fsv); + if (res == -1) { + // Under this condition, the file didn't have fsverity enabled or the + // kernel doesn't support it at all. We need to compute it in the current process. + if (errno == ENODATA || errno == EOPNOTSUPP || errno == ENOTTY) { + // For consistency ensure we start from the beginning. We could + // avoid this by using pread() in the future. + if (lseek(fd, 0, SEEK_SET) < 0) + return -errno; + return lcfs_compute_fsverity_from_fd(digest, fd); + } + // In this case, we found an unexpected error + return -errno; + } + // The file has fsverity enabled, but with an unexpected different algorithm (e.g. sha512). + // This is going to be a weird corner case. For now, we error out. + if (buf.fsv.digest_size != LCFS_DIGEST_SIZE) { + return -EWRONGVERITY; + } + + memcpy(digest, buf.buf, LCFS_DIGEST_SIZE); + + return 0; +} + int lcfs_compute_fsverity_from_data(uint8_t *digest, uint8_t *data, size_t data_len) { FsVerityContext *ctx; diff --git a/libcomposefs/lcfs-writer.h b/libcomposefs/lcfs-writer.h index e205f900..756e0e2d 100644 --- a/libcomposefs/lcfs-writer.h +++ b/libcomposefs/lcfs-writer.h @@ -153,6 +153,7 @@ LCFS_EXTERN int lcfs_compute_fsverity_from_content(uint8_t *digest, void *file, LCFS_EXTERN int lcfs_compute_fsverity_from_fd(uint8_t *digest, int fd); LCFS_EXTERN int lcfs_compute_fsverity_from_data(uint8_t *digest, uint8_t *data, size_t data_len); +LCFS_EXTERN int lcfs_fd_get_fsverity(uint8_t *digest, int fd); LCFS_EXTERN int lcfs_node_set_from_content(struct lcfs_node_s *node, int dirfd, const char *fname, int buildflags); diff --git a/man/composefs-info.md b/man/composefs-info.md index 7b62f0f7..b57705e6 100644 --- a/man/composefs-info.md +++ b/man/composefs-info.md @@ -31,6 +31,9 @@ several sub-commands: accepted as input to mkcomposefs if the --from-file option is used. +**measure-file** +: Interpret the provided paths as generic files, and print their fsverity digest. + # OPTIONS The provided *IMAGE* argument must be a composefs file. Multiple images diff --git a/tests/test-lib.sh b/tests/test-lib.sh index 5f9f615f..2e690392 100644 --- a/tests/test-lib.sh +++ b/tests/test-lib.sh @@ -62,6 +62,13 @@ check_fsverity () { return 0 } +assert_streq () { + if test "$1" != "$2"; then + echo "assertion failed: $1 = $2" 1>&2 + return 1 + fi +} + [[ -v can_whiteout ]] || can_whiteout=$(check_whiteout) [[ -v has_fuse ]] || has_fuse=$(if check_fuse; then echo y; else echo n; fi) [[ -v has_fsck ]] || has_fsck=$(check_erofs_fsck) diff --git a/tests/test-units.sh b/tests/test-units.sh index ae7add97..b53591cd 100755 --- a/tests/test-units.sh +++ b/tests/test-units.sh @@ -68,7 +68,25 @@ function test_mount_digest () { fi } -TESTS="test_inline test_objects test_mount_digest" +function test_composefs_info_measure_files () { + local dir=$1 + cd $dir + + echo hello world > test.txt + echo foo bar baz > test2.txt + composefs-info measure-file test.txt test2.txt > out.txt + assert_streq "$(head -1 out.txt)" "37061ef2ac4c21bec68489b56138c5780306a4ad7fe6676236ecdf2c9027cd92" + assert_streq "$(tail -1 out.txt)" "91e7d88cb7bc9cf6d8db3b0ecf89af4abf204bef5b3ade5113d5b62ef374e70b" + + if [ $has_fsverity = y ]; then + fsverity enable --hash-alg=256 test.txt + digest=$(composefs-info measure-file test.txt) + assert_streq "$digest" "37061ef2ac4c21bec68489b56138c5780306a4ad7fe6676236ecdf2c9027cd92" + fi + cd - +} + +TESTS="test_inline test_objects test_mount_digest test_composefs_info_measure_files" res=0 for i in $TESTS; do testdir=$(mktemp -d $workdir/$i.XXXXXX) diff --git a/tools/composefs-info.c b/tools/composefs-info.c index a8f9c97a..8d4990bd 100644 --- a/tools/composefs-info.c +++ b/tools/composefs-info.c @@ -361,12 +361,45 @@ static void print_objects_handler_end(void *_data) static void usage(const char *argv0) { fprintf(stderr, - "usage: %s [--basedir=path] [ls|objects|dump|missing-objects] IMAGES...\n", + "usage: %s [--basedir=path] [ls|objects|dump|missing-objects|measure-file] IMAGES...\n", argv0); } #define OPT_BASEDIR 100 +// Most of the rest of this code operates on composefs superblocks. This function +// just prints the fsverity digest of the provided files. +static int measure_files(const char *bin, int argc, char **argv) +{ + if (argc <= 2) { + fprintf(stderr, "No files specified\n"); + usage(bin); + exit(1); + } + + for (int i = 2; i < argc; i++) { + const char *path = argv[i]; + uint8_t digest[LCFS_DIGEST_SIZE]; + + cleanup_fd int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + err(EXIT_FAILURE, "Failed to open '%s'", path); + } + + int r = lcfs_fd_get_fsverity(digest, fd); + if (r != 0) { + const char *errmsg = strerror(errno); + fprintf(stderr, "failed to measure '%s': %s", path, errmsg); + exit(1); + } + char digest_str[LCFS_DIGEST_SIZE * 2 + 1] = { 0 }; + digest_to_string(digest, digest_str); + printf("%s\n", digest_str); + } + + return 0; +} + int main(int argc, char **argv) { const char *bin = argv[0]; @@ -420,6 +453,8 @@ int main(int argc, char **argv) handler = print_missing_objects_handler; handler_init = print_objects_handler_init; handler_end = print_objects_handler_end; + } else if (strcmp(command, "measure-file") == 0) { + return measure_files(bin, argc, argv); } else { errx(EXIT_FAILURE, "Unknown command '%s'\n", command); }