# fsverity-utils 1.5 on wasm32-hwjs glibc (BRINTOS-945). # # WHAT SHIPS, AND WHY THE SUITE HAS TWO HALVES. The port installs a tool # (/usr/bin/fsverity) and a library (/usr/lib/libfsverity.a + libfsverity.h + # libfsverity.pc). The CLI half below drives the tool; the probe half drives the # library through data/fsverity-probe, cross-linked against the very archive # that ships. A payload-shape suite would asserts that those files exist and # nothing more, and for a hashing library that is not coverage: its failure mode # is answering WRONGLY -- an off-by-one in a Merkle level walk, a mis-packed # descriptor field, a wrong-endian block count -- and every one of those # compiles, packages and installs perfectly. BRINTOS-217 (oniguruma) is this # layer's standing precedent for exactly that. # # WHERE THE EXPECTED DIGESTS COME FROM, WHICH IS THE POINT OF THIS SUITE. # They are the fs-verity SPECIFICATION's values, not the tool's. Each was # produced two independent ways that agree exactly: # # 1. a native x86_64 build of this same release (git 20e87c1, tag v1.5, # the SRCREV the recipe pins), and # 2. a from-scratch reimplementation of the Merkle tree and the descriptor # hash out of Documentation/filesystems/fsverity.rst, using Python's # hashlib alone and sharing no code with fsverity-utils # (src/fsverity_specdigest.py, committed beside the probe). # # A value only the tool produces records behaviour; a value the tool and the # specification independently produce records correctness. That distinction is # why this suite can claim the port computes fs-verity digests rather than # merely that it computes them the same way twice. # # THE FIXTURE is data/fsverity-fixture.bin -- 131072 bytes from a fully # specified integer recurrence (src/fsverity_mkfixture.py), so it is # byte-identical everywhere and regenerable. 128 KiB puts the Merkle tree more # than one level deep at every block size exercised here (128 blocks at 1024, # 32 at 4096, 2 at 65536), so a single-level implementation cannot pass. # EDITING THE FIXTURE INVALIDATES EVERY DIGEST BELOW: regenerate them from the # spec script, do not copy what the tool prints. # # THE .so IS DELIBERATELY ABSENT and is asserted absent. wasmld refuses a # -shared side-module link handed native wasm inputs, this distro is static-only # with no dlopen, and the bbappend therefore declines to build libfsverity.so. # A .so appearing here means that decision was silently reverted. # # ON `enable` AND `measure`, WHICH ARE THE KERNEL HALF AND ARE *NOT* A BRINTOS # GAP. fs-verity is a filesystem feature: FS_IOC_ENABLE_VERITY returns # EOPNOTSUPP on any filesystem that does not implement it, and a stock x86_64 # Linux on ext4-without-the-verity-feature answers exactly the same way -- the # native control returns "Operation not supported" for both commands. So the # cases below assert that the call REACHES the kernel and is refused cleanly # with the documented error, which is the correct behaviour here and not a # defect to attribute. What would be a finding is a trap, a hang, or a claim of # success; those are what these cases are shaped to catch. . "$BST_LIB" require fsverity fsverity-utils/present hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "absent or empty: $2"; fi; } has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2" ;; esac; } nofile() { if [ -e "$2" ]; then bst_notok "$1" "present but must not be: $2"; else bst_ok "$1"; fi; } # has2 : ok iff EITHER needle is present. # Used where the correct answer is platform-dependent but the SET of correct # answers is not -- see the kernel-half note in the header. has2() { case "$4" in *"$2"*|*"$3"*) bst_ok "$1" ;; *) bst_notok "$1" "neither '$2' nor '$3' found in: $(_bst_1line "$4")" ;; esac } tmp=${TMPDIR:-/tmp} work=$tmp/bst-fsverity.$$ mkdir -p "$work" 2>/dev/null FIX=$BST_DATA/fsverity-fixture.bin # --- payload ----------------------------------------------------------------- hasfile fsverity-utils/bin /usr/bin/fsverity hasfile fsverity-utils/staticlib /usr/lib/libfsverity.a hasfile fsverity-utils/header /usr/include/libfsverity.h hasfile fsverity-utils/pkgconfig /usr/lib/pkgconfig/libfsverity.pc hasfile fsverity-utils/fixture "$FIX" # The port's one deliberate divergence, pinned so it cannot be undone in silence. nofile fsverity-utils/no-sharedlib /usr/lib/libfsverity.so nofile fsverity-utils/no-sharedlib-so0 /usr/lib/libfsverity.so.0 # The fixture must be the exact 131072 bytes the digests below describe. check_eq fsverity-utils/fixture-size 131072 "$(wc -c < "$FIX" 2>/dev/null | tr -d ' ')" # --- the tool's identity ----------------------------------------------------- check_out fsverity-utils/version "fsverity v1.5" fsverity --version # --- the digest matrix, via the CLI ------------------------------------------ # Each value is the spec's; see the header. --compact prints the bare hex. check_eq fsverity-utils/digest-sha256-1024 \ f34b35c43333dcd19419def72f97a5f2a8f67595872fb95f66c5e3460123ffe3 \ "$(fsverity digest "$FIX" --compact --hash-alg=sha256 --block-size=1024 2>&1)" check_eq fsverity-utils/digest-sha256-4096 \ b18ebfb4d7c5faabb393c7c05d4798a00cdf11f8d1229c18f11241b563e0f26f \ "$(fsverity digest "$FIX" --compact --hash-alg=sha256 --block-size=4096 2>&1)" check_eq fsverity-utils/digest-sha256-65536 \ 73d7903004c073c2ad8d1f06f5b736759dafe0fa5cafa2978ba928581078a3c3 \ "$(fsverity digest "$FIX" --compact --hash-alg=sha256 --block-size=65536 2>&1)" check_eq fsverity-utils/digest-sha512-1024 \ 1728ca749f472843c3786f2b467a29f431a4c20c6976f415b47243934c42676aa5e5bc110fb2fff8a04e021662c7c2a4397e68547b5d6578d4834e6140ebb843 \ "$(fsverity digest "$FIX" --compact --hash-alg=sha512 --block-size=1024 2>&1)" check_eq fsverity-utils/digest-sha512-4096 \ ac3d8b80724686b89f43066355e6356e9978d9cde0de4ed51d9282daa15504b0e551d65e909c3dd59b153f815a7a26a099643a113bfc8122623ed48efa0dd293 \ "$(fsverity digest "$FIX" --compact --hash-alg=sha512 --block-size=4096 2>&1)" check_eq fsverity-utils/digest-sha512-65536 \ 1d4c8a0d0903857169ee36849213a95a351823ae82fabeb3aea76aaf9c570338ce51f12a7f0f750cc7257b53ff8ba347bf6aba2ae7d3dd000be1087f0621ca44 \ "$(fsverity digest "$FIX" --compact --hash-alg=sha512 --block-size=65536 2>&1)" # The salt must CHANGE the digest of identical bytes -- an implementation that # accepted --salt and ignored it would pass every case above. check_eq fsverity-utils/digest-salted \ d5a4898df0104effd883da4476b335c691592f3f3993cc3b30bb1e617ac04517 \ "$(fsverity digest "$FIX" --compact --hash-alg=sha256 --block-size=4096 --salt=00112233445566778899aabbccddeeff 2>&1)" # No flags at all must mean sha256/4096 and reach the same answer. check_eq fsverity-utils/digest-defaults \ b18ebfb4d7c5faabb393c7c05d4798a00cdf11f8d1229c18f11241b563e0f26f \ "$(fsverity digest "$FIX" --compact 2>&1)" # The non-compact form is the one a human reads; assert its shape too. check_out fsverity-utils/digest-longform \ "sha256:b18ebfb4d7c5faabb393c7c05d4798a00cdf11f8d1229c18f11241b563e0f26f" \ fsverity digest "$FIX" # --- the signed-content layout ------------------------------------------------ # --for-builtin-sig prints exactly what fs-verity signs. The 12-byte prefix is # derived from the struct in the spec ("FSVerity", then algorithm and digest # size as little-endian 16-bit fields), not copied from this tool's output. fbs=$(fsverity digest "$FIX" --compact --for-builtin-sig 2>&1) check_eq fsverity-utils/sig-content-prefix 465356657269747901002000 "$(echo "$fbs" | cut -c1-24)" check_eq fsverity-utils/sig-content-digest \ b18ebfb4d7c5faabb393c7c05d4798a00cdf11f8d1229c18f11241b563e0f26f \ "$(echo "$fbs" | cut -c25-)" # --- descriptor and Merkle tree ------------------------------------------------ # THESE TWO ARE A KNOWN RED ON THIS PLATFORM, and they are left as real checks # rather than skips so they turn green by themselves when it is fixed. # # fsverity.c:217 is the ONLY caller of preallocate_file(), and it is on the # --out-merkle-tree path; preallocate_file() is posix_fallocate(), which # returns ENOSYS (38) on this guest -- brintos/linux#91: __NR_fallocate (285) is # defined in the wasm32 UAPI but unwired in the syscall table, and glibc falls # back to its portable write-loop only on EOPNOTSUPP, so the errno reaches the # caller. The command aborts there, so the descriptor # is never written either and both sizes read 0. Everything that does NOT # preallocate -- every digest above, and signing, which uses full_write -- # works, which is why this gap costs the port two cases and nothing else. Both # turn green by themselves when brintos/linux#91 is fixed. # # DO NOT TRUST THE TOOL'S OWN ERROR TEXT HERE. It prints # # ERROR: preallocating 4096-byte file '...': Cannot allocate memory # # and that is the WRONG errno. posix_fallocate returns its error directly and # is specified NOT to set errno; preallocate_file() captures that return value # and then reports through error_msg_errno(), which formats the GLOBAL errno -- # a stale ENOMEM (12) left by an earlier call. The real value is the 38 the # probe reports below, and reading the message instead of the return value # sends a diagnosis after a memory problem this platform does not have. That # reporting bug is upstream's, is recorded in the design doc's Issues Found, # and is one reason the probe calls posix_fallocate directly rather than # inferring the platform's answer from this text. fsverity digest "$FIX" --out-descriptor="$work/desc.bin" \ --out-merkle-tree="$work/tree.bin" >/dev/null 2>&1 check_eq fsverity-utils/descriptor-size 256 "$(wc -c < "$work/desc.bin" 2>/dev/null | tr -d ' ')" check_eq fsverity-utils/merkle-tree-size 4096 "$(wc -c < "$work/tree.bin" 2>/dev/null | tr -d ' ')" # --- refusals. A tool that accepts nonsense is as broken as one that computes # it wrongly, and these are the arguments a caller gets wrong. ------------- check_rc fsverity-utils/reject-blocksize 2 fsverity digest "$FIX" --block-size=4095 check_rc fsverity-utils/reject-hash-alg 2 fsverity digest "$FIX" --hash-alg=sha3-512 check_rc fsverity-utils/reject-salt-hex 2 fsverity digest "$FIX" --salt=zz check_rc fsverity-utils/reject-missing 1 fsverity digest /nonexistent-fsverity-target # --- the kernel half. See the header: EOPNOTSUPP is the documented answer on a # filesystem without fs-verity, and the native control gives the same. ---- # The errno is platform-dependent and BOTH answers below are correct, which is # why this asserts the set rather than one member. EOPNOTSUPP ("Operation not # supported") is what a filesystem that knows the ioctl but lacks the feature # returns -- the native x86_64 control on ext4 gives exactly that. ENOTTY # ("Inappropriate ioctl for device") is what a filesystem with no fs-verity # support at all returns, because the command falls off the end of the ioctl # dispatch; that is this guest's answer, and it is equally documented. Pinning # either one alone would make a correct platform fail. # # What is genuinely asserted is the BEHAVIOUR: the call reaches the kernel, is # refused with status 1, and the tool names the ioctl it tried. A trap, a hang, # or a claim of success is what these cases exist to catch. printf 'fsverity kernel-half target\n' > "$work/plain.bin" 2>/dev/null out=$(fsverity enable "$work/plain.bin" 2>&1); rc=$? check_eq fsverity-utils/enable-refused 1 "$rc" has2 fsverity-utils/enable-documented-errno \ "Operation not supported" "Inappropriate ioctl for device" "$out" has fsverity-utils/enable-names-ioctl "FS_IOC_ENABLE_VERITY" "$out" out=$(fsverity measure "$work/plain.bin" 2>&1); rc=$? check_eq fsverity-utils/measure-refused 1 "$rc" has2 fsverity-utils/measure-documented-errno \ "Operation not supported" "Inappropriate ioctl for device" "$out" has fsverity-utils/measure-names-ioctl "FS_IOC_MEASURE_VERITY" "$out" # --- key material, generated here because BOTH halves below need it ---------- # The ticket's requirement is to sign the digest with a key the suite generates # and verify that signature with openssl. The key is made first so the probe can # drive libfsverity_sign_digest() with it, and the CLI signature and the probe's # signed-content file can then be checked against each other. HAVE_KEYS=0 if command -v openssl >/dev/null 2>&1; then if openssl req -newkey rsa:2048 -nodes -keyout "$work/k.pem" -x509 -days 3650 \ -out "$work/c.pem" -subj "/CN=brintOS fsverity selftest" >/dev/null 2>&1 \ && [ -s "$work/k.pem" ] && [ -s "$work/c.pem" ]; then bst_ok fsverity-utils/keygen HAVE_KEYS=1 else bst_notok fsverity-utils/keygen "openssl could not generate a key and certificate" fi else bst_skip fsverity-utils/keygen "openssl not installed on this image" fi # --- the probe: the library computing, on target ----------------------------- hasfile fsverity-utils/probe-payload "$BST_DATA/fsverity-probe" probe=$work/fsverity-probe if cp "$BST_DATA/fsverity-probe" "$probe" 2>/dev/null && chmod +x "$probe" 2>/dev/null; then out=$("$probe" "$FIX" "$work" "$work/k.pem" "$work/c.pem" 2>&1) has fsverity-utils/run-fixture-size "FSV-fixture-size OK" "$out" # algorithm metadata has fsverity-utils/run-alg-sha256-size "FSV-alg-sha256-size OK" "$out" has fsverity-utils/run-alg-sha512-size "FSV-alg-sha512-size OK" "$out" has fsverity-utils/run-alg-unknown-size "FSV-alg-unknown-size OK" "$out" has fsverity-utils/run-alg-sha256-name "FSV-alg-sha256-name OK" "$out" has fsverity-utils/run-alg-sha512-name "FSV-alg-sha512-name OK" "$out" has fsverity-utils/run-alg-unknown-name "FSV-alg-unknown-name OK" "$out" has fsverity-utils/run-alg-by-name-256 "FSV-alg-by-name-sha256 OK" "$out" has fsverity-utils/run-alg-by-name-512 "FSV-alg-by-name-sha512 OK" "$out" has fsverity-utils/run-alg-by-name-bogus "FSV-alg-by-name-bogus OK" "$out" # the library's digests -- the same six values the CLI half computed, reached # through libfsverity_compute_digest and its read callback instead has fsverity-utils/run-digest-sha256-1024 "FSV-digest-sha256-1024 OK" "$out" has fsverity-utils/run-digest-sha256-4096 "FSV-digest-sha256-4096 OK" "$out" has fsverity-utils/run-digest-sha256-65536 "FSV-digest-sha256-65536 OK" "$out" has fsverity-utils/run-digest-sha512-1024 "FSV-digest-sha512-1024 OK" "$out" has fsverity-utils/run-digest-sha512-4096 "FSV-digest-sha512-4096 OK" "$out" has fsverity-utils/run-digest-sha512-65536 "FSV-digest-sha512-65536 OK" "$out" has fsverity-utils/run-digest-salted "FSV-digest-salted OK" "$out" has fsverity-utils/run-digest-defaults "FSV-digest-defaults OK" "$out" # the digest object's own fields, and the read callback's behaviour has fsverity-utils/run-digest-rc0 "FSV-digest-rc0 OK" "$out" has fsverity-utils/run-digest-alg-field "FSV-digest-alg-field OK" "$out" has fsverity-utils/run-digest-size-field "FSV-digest-size-field OK" "$out" has fsverity-utils/run-read-consumed-all "FSV-read-consumed-all OK" "$out" has fsverity-utils/run-read-chunked "FSV-read-chunked OK" "$out" # the library must agree with the tool, byte for byte has fsverity-utils/run-digest-value \ "FSV-digest-value b18ebfb4d7c5faabb393c7c05d4798a00cdf11f8d1229c18f11241b563e0f26f" "$out" # refusals has fsverity-utils/run-reject-version0 "FSV-reject-version0 OK" "$out" has fsverity-utils/run-reject-blocksize "FSV-reject-blocksize-npot OK" "$out" has fsverity-utils/run-reject-alg-unknown "FSV-reject-alg-unknown OK" "$out" # signing through the library has fsverity-utils/run-sign-rc0 "FSV-sign-rc0 OK" "$out" has fsverity-utils/run-sign-nonempty "FSV-sign-nonempty OK" "$out" has fsverity-utils/run-sign-der-sequence "FSV-sign-der-sequence OK" "$out" has fsverity-utils/run-sign-written "FSV-sign-written OK" "$out" has fsverity-utils/run-sign-content "FSV-sign-content-written OK" "$out" has fsverity-utils/run-sign-reject-nokey "FSV-sign-reject-nokey OK" "$out" # posix_fallocate, reduced to the bare syscall. This is the gap that reddens # descriptor-size and merkle-tree-size above; asserting it here is what makes # those two attributable to a platform limitation rather than to this port. # Carry the errno in the failure reason rather than just "not found": this # case is the one a reader lands on when the port is red, and the number is # the whole finding. posix_fallocate returns the error directly rather than # through errno, so the value below IS the error -- and is the trustworthy # one, unlike the tool's own message (see the header). frc=$(printf '%s\n' "$out" | sed -n 's/^FSV-falloc-rc //p' | head -1) case "$out" in *"FSV-falloc-4096 OK"*) bst_ok fsverity-utils/run-falloc ;; *) bst_notok fsverity-utils/run-falloc \ "brintos/linux#91: posix_fallocate(fd,0,4096) returned ${frc:-?} (ENOSYS=38; __NR_fallocate 285 unwired); ftruncate on the same fd is unaffected, and the tool's own ENOMEM message is wrong -- see the header" ;; esac has fsverity-utils/run-ftruncate "FSV-ftruncate-4096 OK" "$out" # libfsverity_enable: reached the kernel and was refused, rather than trapping has fsverity-utils/run-enable-returned "FSV-enable-returned OK" "$out" has fsverity-utils/run-enable-unsupported "FSV-enable-unsupported OK" "$out" # The probe's own bottom line. It counts posix_fallocate among its failures, # so on a platform carrying that gap this is red BY CONSTRUCTION and is the # same finding as run-falloc rather than an extra one; it goes green with it. fcount=$(printf '%s\n' "$out" | sed -n 's/^FSV-DONE failures=//p' | head -1) case "$out" in *"FSV-DONE failures=0"*) bst_ok fsverity-utils/run-no-failures ;; *) bst_notok fsverity-utils/run-no-failures \ "the probe reported ${fcount:-?} failure(s); while brintos/linux#91 stands the expected count is exactly 1 (run-falloc), so anything else is a NEW finding" ;; esac else bst_notok fsverity-utils/probe-stage "cannot stage $BST_DATA/fsverity-probe" fi # --- signing, and verifying the signature with openssl ------------------------ # Both tools must agree about the 44 bytes under signature, which is what makes # this more than a self-consistency check: the CLI reports them through # --for-builtin-sig, the library wrote them from its own struct, and openssl # verifies the CLI's signature against the library's file. if [ "$HAVE_KEYS" = 1 ]; then check fsverity-utils/sign fsverity sign "$FIX" "$work/cli.sig" \ --key="$work/k.pem" --cert="$work/c.pem" hasfile fsverity-utils/sign-output "$work/cli.sig" # A PKCS#7 message in DER begins with a SEQUENCE tag. check_eq fsverity-utils/sign-der-sequence 30 \ "$(od -An -N1 -tx1 "$work/cli.sig" 2>/dev/null | tr -d ' \n')" check_out fsverity-utils/sign-is-pkcs7 "pkcs7-signedData" \ openssl asn1parse -inform DER -in "$work/cli.sig" if [ -s "$work/fsv-probe-content.bin" ]; then check_eq fsverity-utils/sig-content-agrees "$fbs" \ "$(od -An -v -tx1 "$work/fsv-probe-content.bin" 2>/dev/null | tr -d ' \n')" check_out fsverity-utils/openssl-verify "Verification successful" \ openssl smime -verify -inform DER -in "$work/cli.sig" \ -content "$work/fsv-probe-content.bin" \ -certfile "$work/c.pem" -CAfile "$work/c.pem" -purpose any -out /dev/null # The negative control for the case above. Verification against DIFFERENT # content must fail; without this, a verify that succeeded unconditionally # would read exactly like a verify that worked. cp "$work/fsv-probe-content.bin" "$work/tampered.bin" 2>/dev/null printf 'X' | dd of="$work/tampered.bin" bs=1 seek=43 conv=notrunc status=none 2>/dev/null if openssl smime -verify -inform DER -in "$work/cli.sig" \ -content "$work/tampered.bin" -certfile "$work/c.pem" \ -CAfile "$work/c.pem" -purpose any -out /dev/null >/dev/null 2>&1; then bst_notok fsverity-utils/openssl-verify-tampered \ "openssl verified a signature over tampered content" else bst_ok fsverity-utils/openssl-verify-tampered fi else bst_notok fsverity-utils/sig-content-agrees "the probe did not write its signed-content file" bst_notok fsverity-utils/openssl-verify "the probe did not write its signed-content file" bst_notok fsverity-utils/openssl-verify-tampered "the probe did not write its signed-content file" fi # Signing with a certificate and no key must be refused, not silently produce # something nothing can verify. check_rc fsverity-utils/sign-reject-nokey 1 \ fsverity sign "$FIX" "$work/nokey.sig" --cert="$work/c.pem" else for c in sign sign-output sign-der-sequence sign-is-pkcs7 sig-content-agrees \ openssl-verify openssl-verify-tampered sign-reject-nokey; do bst_skip "fsverity-utils/$c" "no key material (openssl unavailable)" done fi # --- pkg-config contents ------------------------------------------------------ pc=$(cat /usr/lib/pkgconfig/libfsverity.pc 2>/dev/null) has fsverity-utils/pc-name "Name: libfsverity" "$pc" has fsverity-utils/pc-version "Version: 1.5" "$pc" has fsverity-utils/pc-libs "-lfsverity" "$pc" case "$pc" in */home/*|*"$work"*|*tmp-glibc*) bst_notok fsverity-utils/pc-no-buildpath "a build path leaked into libfsverity.pc" ;; *) bst_ok fsverity-utils/pc-no-buildpath ;; esac rm -rf "$work" 2>/dev/null