#!/bin/bash # SPDX-License-Identifier: GPL-2.0 # # Test script for dm-verity keyring functionality # # This script has two modes depending on kernel configuration: # # 1. keyring_unsealed=1 AND require_signatures=1: # - Upload a test key to the .dm-verity keyring # - Seal the keyring # - Create a dm-verity device with a signed root hash # - Verify signature verification works # # 2. keyring_unsealed=0 (default) OR require_signatures=0: # - Verify the keyring is already sealed (if unsealed=0) # - Verify keys cannot be added to a sealed keyring # - Verify the keyring is inactive (not used for verification) # # Requirements: # - Root privileges # - openssl # - veritysetup (cryptsetup) # - keyctl (keyutils) set -e WORK_DIR="" DATA_DEV="" HASH_DEV="" DM_NAME="verity-test-$$" CLEANUP_DONE=0 # Module parameters (detected at runtime) KEYRING_UNSEALED="" REQUIRE_SIGNATURES="" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color log_info() { echo -e "${GREEN}[INFO]${NC} $*" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2 } log_pass() { echo -e "${GREEN}[PASS]${NC} $*" } log_fail() { echo -e "${RED}[FAIL]${NC} $*" >&2 } log_skip() { echo -e "${YELLOW}[SKIP]${NC} $*" } cleanup() { if [ "$CLEANUP_DONE" -eq 1 ]; then return fi CLEANUP_DONE=1 log_info "Cleaning up..." # Remove dm-verity device if it exists if dmsetup info "$DM_NAME" &>/dev/null; then dmsetup remove "$DM_NAME" 2>/dev/null || true fi # Detach loop devices if [ -n "$DATA_DEV" ] && [[ "$DATA_DEV" == /dev/loop* ]]; then losetup -d "$DATA_DEV" 2>/dev/null || true fi if [ -n "$HASH_DEV" ] && [[ "$HASH_DEV" == /dev/loop* ]]; then losetup -d "$HASH_DEV" 2>/dev/null || true fi # Remove work directory if [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ]; then rm -rf "$WORK_DIR" fi } trap cleanup EXIT die() { log_error "$*" exit 1 } find_dm_verity_keyring() { # The .dm-verity keyring is not linked to user-accessible keyrings, # so we need to find it via /proc/keys local serial_hex serial_hex=$(awk '/\.dm-verity/ {print $1}' /proc/keys 2>/dev/null) if [ -z "$serial_hex" ]; then return 1 fi # Convert hex to decimal for keyctl echo $((16#$serial_hex)) } get_module_param() { local param="$1" local path="/sys/module/dm_verity/parameters/$param" if [ -f "$path" ]; then cat "$path" else echo "" fi } check_requirements() { log_info "Checking requirements..." # Check for root if [ "$(id -u)" -ne 0 ]; then die "This script must be run as root" fi # Check for required tools for cmd in openssl veritysetup keyctl losetup dmsetup dd awk; do if ! command -v "$cmd" &>/dev/null; then die "Required command not found: $cmd" fi done # Check for dm-verity module if ! modprobe -n dm-verity &>/dev/null; then die "dm-verity module not available" fi # Verify OpenSSL can create signatures # OpenSSL cms -sign with -binary -outform DER creates detached signatures by default log_info "Using OpenSSL for PKCS#7 signatures" } load_dm_verity_module() { local keyring_unsealed="${1:-0}" local require_signatures="${2:-0}" log_info "Loading dm-verity module with keyring_unsealed=$keyring_unsealed require_signatures=$require_signatures" # Unload if already loaded if lsmod | grep -q '^dm_verity'; then log_info "Unloading existing dm-verity module..." modprobe -r dm-verity 2>/dev/null || \ die "Failed to unload dm-verity module (may be in use)" sleep 1 fi # Load with specified parameters modprobe dm-verity keyring_unsealed="$keyring_unsealed" require_signatures="$require_signatures" || \ die "Failed to load dm-verity module" # Wait for keyring to be created (poll with timeout) local keyring_id="" local timeout=50 # 5 seconds (50 * 0.1s) while [ $timeout -gt 0 ]; do keyring_id=$(find_dm_verity_keyring) && break sleep 0.1 timeout=$((timeout - 1)) done if [ -z "$keyring_id" ]; then die "dm-verity keyring not found after module load (timeout)" fi log_info "Found .dm-verity keyring: $keyring_id" echo "$keyring_id" > "$WORK_DIR/keyring_id" # Read and display module parameters KEYRING_UNSEALED=$(get_module_param "keyring_unsealed") REQUIRE_SIGNATURES=$(get_module_param "require_signatures") log_info "Module parameters:" log_info " keyring_unsealed=$KEYRING_UNSEALED" log_info " require_signatures=$REQUIRE_SIGNATURES" } unload_dm_verity_module() { log_info "Unloading dm-verity module..." # Clean up any dm-verity devices first local dm_dev while read -r dm_dev _; do [ -n "$dm_dev" ] || continue log_info "Removing dm-verity device: $dm_dev" dmsetup remove "$dm_dev" 2>/dev/null || true done < <(dmsetup ls --target verity 2>/dev/null) if lsmod | grep -q '^dm_verity'; then modprobe -r dm-verity 2>/dev/null || \ log_warn "Failed to unload dm-verity module" sleep 1 fi } generate_keys() { log_info "Generating signing key pair..." # Generate private key (2048-bit for faster test execution) openssl genrsa -out "$WORK_DIR/private.pem" 2048 2>/dev/null # Create OpenSSL config for certificate extensions # The kernel requires digitalSignature key usage for signature verification # Both subjectKeyIdentifier and authorityKeyIdentifier are needed for # the kernel to match keys in the keyring (especially for self-signed certs) cat > "$WORK_DIR/openssl.cnf" << 'EOF' [req] distinguished_name = req_distinguished_name x509_extensions = v3_ca prompt = no [req_distinguished_name] CN = dm-verity-test-key [v3_ca] basicConstraints = critical,CA:FALSE keyUsage = digitalSignature subjectKeyIdentifier = hash authorityKeyIdentifier = keyid EOF # Generate self-signed certificate with proper extensions openssl req -new -x509 -key "$WORK_DIR/private.pem" \ -out "$WORK_DIR/cert.pem" -days 365 \ -config "$WORK_DIR/openssl.cnf" 2>/dev/null # Convert certificate to DER format for kernel openssl x509 -in "$WORK_DIR/cert.pem" -outform DER \ -out "$WORK_DIR/cert.der" # Show certificate info for debugging log_info "Certificate details:" openssl x509 -in "$WORK_DIR/cert.pem" -noout -text 2>/dev/null | \ grep -E "Subject:|Issuer:|Key Usage|Extended" | head -10 log_info "Keys generated successfully" } seal_keyring() { log_info "Sealing the .dm-verity keyring..." local keyring_id keyring_id=$(cat "$WORK_DIR/keyring_id") keyctl restrict_keyring "$keyring_id" || \ die "Failed to seal keyring" log_info "Keyring sealed successfully" } create_test_device() { log_info "Creating test device images..." # Create data image with random content (8MB is sufficient for testing) dd if=/dev/urandom of="$WORK_DIR/data.img" bs=1M count=8 status=none # Create hash image (will be populated by veritysetup) dd if=/dev/zero of="$WORK_DIR/hash.img" bs=1M count=1 status=none # Setup loop devices DATA_DEV=$(losetup --find --show "$WORK_DIR/data.img") HASH_DEV=$(losetup --find --show "$WORK_DIR/hash.img") log_info "Data device: $DATA_DEV" log_info "Hash device: $HASH_DEV" } create_verity_hash() { log_info "Creating dm-verity hash tree..." local root_hash output output=$(veritysetup format "$DATA_DEV" "$HASH_DEV" 2>&1) root_hash=$(echo "$output" | grep "Root hash:" | awk '{print $3}') if [ -z "$root_hash" ]; then log_error "veritysetup format output:" echo "$output" | sed 's/^/ /' die "Failed to get root hash from veritysetup format" fi echo "$root_hash" > "$WORK_DIR/root_hash" log_info "Root hash: $root_hash" } create_detached_signature() { local infile="$1" local outfile="$2" local cert="$3" local key="$4" # Use openssl smime (not cms) for PKCS#7 signatures compatible with kernel # Flags from working veritysetup example: # -nocerts: don't include certificate in signature # -noattr: no signed attributes # -binary: binary input mode if openssl smime -sign -nocerts -noattr -binary \ -in "$infile" \ -inkey "$key" \ -signer "$cert" \ -outform der \ -out "$outfile" 2>/dev/null; then return 0 fi log_error "Failed to create signature" return 1 } activate_verity_device() { local with_sig="$1" local root_hash root_hash=$(cat "$WORK_DIR/root_hash") # Clear dmesg and capture any kernel messages during activation dmesg -C 2>/dev/null || true if [ "$with_sig" = "yes" ]; then log_info "Activating dm-verity device with signature..." veritysetup open "$DATA_DEV" "$DM_NAME" "$HASH_DEV" "$root_hash" \ --root-hash-signature="$WORK_DIR/root_hash.p7s" 2>&1 local ret=$? else log_info "Activating dm-verity device without signature..." veritysetup open "$DATA_DEV" "$DM_NAME" "$HASH_DEV" "$root_hash" 2>&1 local ret=$? fi # Show relevant kernel messages local kmsg kmsg=$(dmesg 2>/dev/null | grep -i -E 'verity|pkcs|signature|asymmetric|key' | tail -10) if [ -n "$kmsg" ]; then log_info "Kernel messages:" echo "$kmsg" | while read -r line; do echo " $line"; done fi return $ret } deactivate_verity_device() { if dmsetup info "$DM_NAME" &>/dev/null; then dmsetup remove "$DM_NAME" 2>/dev/null || true fi } show_keyring_status() { log_info "Keyring status:" local keyring_id keyring_id=$(find_dm_verity_keyring) || true if [ -n "$keyring_id" ]; then echo " Keyring ID: $keyring_id" keyctl show "$keyring_id" 2>/dev/null || true grep '\.dm-verity' /proc/keys 2>/dev/null || true fi } list_keyring_keys() { log_info "Keys in .dm-verity keyring:" local keyring_id keyring_id=$(cat "$WORK_DIR/keyring_id" 2>/dev/null) || \ keyring_id=$(find_dm_verity_keyring) || true if [ -z "$keyring_id" ]; then log_warn "Could not find keyring" return fi # List all keys in the keyring local keys keys=$(keyctl list "$keyring_id" 2>/dev/null) if [ -z "$keys" ] || [ "$keys" = "keyring is empty" ]; then echo " (empty)" else echo "$keys" | while read -r line; do echo " $line" done # Show detailed info for each key log_info "Key details:" keyctl list "$keyring_id" 2>/dev/null | awk '{print $1}' | grep -E '^[0-9]+$' | while read -r key_id; do echo " Key $key_id:" keyctl describe "$key_id" 2>/dev/null | sed 's/^/ /' done fi } generate_named_key() { local name="$1" local key_dir="$WORK_DIR/keys/$name" mkdir -p "$key_dir" # Log to stderr so it doesn't interfere with return value echo "[INFO] Generating key pair: $name" >&2 # Generate private key openssl genrsa -out "$key_dir/private.pem" 2048 2>/dev/null # Create OpenSSL config for certificate extensions # Both subjectKeyIdentifier and authorityKeyIdentifier are needed for # the kernel to match keys in the keyring (especially for self-signed certs) cat > "$key_dir/openssl.cnf" << EOF [req] distinguished_name = req_distinguished_name x509_extensions = v3_ca prompt = no [req_distinguished_name] CN = dm-verity-test-$name [v3_ca] basicConstraints = critical,CA:FALSE keyUsage = digitalSignature subjectKeyIdentifier = hash authorityKeyIdentifier = keyid EOF # Generate self-signed certificate with proper extensions openssl req -new -x509 -key "$key_dir/private.pem" \ -out "$key_dir/cert.pem" -days 365 \ -config "$key_dir/openssl.cnf" 2>/dev/null # Convert certificate to DER format for kernel openssl x509 -in "$key_dir/cert.pem" -outform DER \ -out "$key_dir/cert.der" # Return the key directory path (only this goes to stdout) echo "$key_dir" } upload_named_key() { local name="$1" local key_dir="$2" local keyring_id keyring_id=$(cat "$WORK_DIR/keyring_id") log_info "Uploading key '$name' to keyring..." local key_id if key_id=$(keyctl padd asymmetric "$name" "$keyring_id" \ < "$key_dir/cert.der" 2>&1); then log_info "Key '$name' uploaded with ID: $key_id" echo "$key_id" > "$key_dir/key_id" return 0 else log_error "Failed to upload key '$name': $key_id" return 1 fi } # # Test: Verify sealed keyring rejects key additions # test_sealed_keyring_rejects_keys() { log_info "TEST: Verify sealed keyring rejects key additions" local keyring_id keyring_id=$(cat "$WORK_DIR/keyring_id") generate_keys # Try to add a key - should fail if keyctl padd asymmetric "dm-verity-test" "$keyring_id" \ < "$WORK_DIR/cert.der" 2>/dev/null; then log_fail "Key addition should have been rejected on sealed keyring" return 1 else log_pass "Sealed keyring correctly rejected key addition" return 0 fi } # # Test: Multiple keys in keyring # test_multiple_keys() { log_info "TEST: Multiple keys in keyring" local key1_dir key2_dir key3_dir # Generate three different keys key1_dir=$(generate_named_key "vendor-a") key2_dir=$(generate_named_key "vendor-b") key3_dir=$(generate_named_key "vendor-c") # Upload all three keys upload_named_key "vendor-a" "$key1_dir" || return 1 upload_named_key "vendor-b" "$key2_dir" || return 1 upload_named_key "vendor-c" "$key3_dir" || return 1 log_info "" log_info "Keys in keyring before sealing:" list_keyring_keys show_keyring_status # Seal the keyring log_info "" seal_keyring # List keys after sealing log_info "" log_info "Keys in keyring after sealing:" list_keyring_keys show_keyring_status log_pass "Key upload and keyring sealing succeeded" # Create test device log_info "" create_test_device create_verity_hash # Test 1: Sign with key1, should verify successfully log_info "" log_info "Sub-test: Verify with vendor-a key" if ! sign_root_hash_with_key "$key1_dir"; then log_fail "Failed to sign with vendor-a key" return 1 fi if activate_verity_device "yes"; then log_pass "Verification with vendor-a key succeeded" deactivate_verity_device else log_fail "Verification with vendor-a key should succeed" return 1 fi # Test 2: Sign with key2, should also verify successfully log_info "" log_info "Sub-test: Verify with vendor-b key" if ! sign_root_hash_with_key "$key2_dir"; then log_fail "Failed to sign with vendor-b key" return 1 fi if activate_verity_device "yes"; then log_pass "Verification with vendor-b key succeeded" deactivate_verity_device else log_fail "Verification with vendor-b key should succeed" return 1 fi # Test 3: Sign with key3, should also verify successfully log_info "" log_info "Sub-test: Verify with vendor-c key" if ! sign_root_hash_with_key "$key3_dir"; then log_fail "Failed to sign with vendor-c key" return 1 fi if activate_verity_device "yes"; then log_pass "Verification with vendor-c key succeeded" deactivate_verity_device else log_fail "Verification with vendor-c key should succeed" return 1 fi # Test 4: Generate a key NOT in the keyring, should fail log_info "" log_info "Sub-test: Verify with unknown key (should fail)" local unknown_key_dir unknown_key_dir=$(generate_named_key "unknown-vendor") if ! sign_root_hash_with_key "$unknown_key_dir"; then log_fail "Failed to sign with unknown-vendor key" return 1 fi if activate_verity_device "yes"; then log_fail "Verification with unknown key should fail" deactivate_verity_device return 1 else log_pass "Verification with unknown key correctly rejected" fi log_info "" log_pass "Multiple keys test completed successfully" return 0 } sign_root_hash_with_key() { local key_dir="$1" local root_hash root_hash=$(cat "$WORK_DIR/root_hash") # Create the data to sign (hex string, not binary) echo -n "$root_hash" > "$WORK_DIR/root_hash.txt" # Debug: show exactly what we're signing log_info "Root hash (hex): $root_hash" log_info "Root hash hex string size: $(wc -c < "$WORK_DIR/root_hash.txt") bytes" # Create detached PKCS#7 signature if ! create_detached_signature "$WORK_DIR/root_hash.txt" "$WORK_DIR/root_hash.p7s" \ "$key_dir/cert.pem" "$key_dir/private.pem"; then log_error "Failed to sign root hash with key from $key_dir" return 1 fi # Debug: show signing certificate info log_info "Signed with certificate:" openssl x509 -in "$key_dir/cert.pem" -noout -subject 2>/dev/null | sed 's/^/ /' # Debug: verify signature locally # -nointern: cert not in signature, use -certfile # -noverify: skip certificate chain validation (self-signed) if openssl smime -verify -binary -inform der -nointern -noverify \ -in "$WORK_DIR/root_hash.p7s" \ -content "$WORK_DIR/root_hash.txt" \ -certfile "$key_dir/cert.pem" \ -out /dev/null 2>/dev/null; then log_info "Local signature verification: PASSED" else log_warn "Local signature verification: FAILED" fi return 0 } # # Test: Verify corrupted signatures are rejected # test_corrupted_signature() { log_info "TEST: Verify corrupted signatures are rejected" # This test requires a valid setup from test_multiple_keys or similar # It modifies the signature file and verifies rejection if [ ! -f "$WORK_DIR/root_hash.p7s" ]; then log_warn "No signature file found, skipping corrupted signature test" return 0 fi # Save original signature cp "$WORK_DIR/root_hash.p7s" "$WORK_DIR/root_hash.p7s.orig" # Test 1: Truncated signature log_info "Sub-test: Truncated signature (should fail)" head -c 100 "$WORK_DIR/root_hash.p7s.orig" > "$WORK_DIR/root_hash.p7s" if activate_verity_device "yes"; then log_fail "Truncated signature should be rejected" deactivate_verity_device cp "$WORK_DIR/root_hash.p7s.orig" "$WORK_DIR/root_hash.p7s" return 1 else log_pass "Truncated signature correctly rejected" fi # Test 2: Corrupted signature (flip some bytes) log_info "Sub-test: Corrupted signature bytes (should fail)" cp "$WORK_DIR/root_hash.p7s.orig" "$WORK_DIR/root_hash.p7s" # Corrupt bytes in the middle of the signature local sig_size sig_size=$(wc -c < "$WORK_DIR/root_hash.p7s") local corrupt_offset=$((sig_size / 2)) printf '\xff\xff\xff\xff' | dd of="$WORK_DIR/root_hash.p7s" bs=1 seek=$corrupt_offset conv=notrunc 2>/dev/null if activate_verity_device "yes"; then log_fail "Corrupted signature should be rejected" deactivate_verity_device cp "$WORK_DIR/root_hash.p7s.orig" "$WORK_DIR/root_hash.p7s" return 1 else log_pass "Corrupted signature correctly rejected" fi # Test 3: Signature over wrong data (sign different content) log_info "Sub-test: Signature over wrong data (should fail)" # Create a different root hash (all zeros as hex string) printf '%064d' 0 > "$WORK_DIR/wrong_hash.txt" # Get the first key directory that was used local key_dir="$WORK_DIR/keys/vendor-a" if [ -d "$key_dir" ]; then create_detached_signature "$WORK_DIR/wrong_hash.txt" "$WORK_DIR/root_hash.p7s" \ "$key_dir/cert.pem" "$key_dir/private.pem" if activate_verity_device "yes"; then log_fail "Signature over wrong data should be rejected" deactivate_verity_device cp "$WORK_DIR/root_hash.p7s.orig" "$WORK_DIR/root_hash.p7s" return 1 else log_pass "Signature over wrong data correctly rejected" fi else log_warn "Key directory not found, skipping wrong data test" fi # Restore original signature cp "$WORK_DIR/root_hash.p7s.orig" "$WORK_DIR/root_hash.p7s" log_pass "Corrupted signature test completed successfully" return 0 } # # Test: Verify keyring is sealed when keyring_unsealed=0 # test_keyring_sealed_by_default() { log_info "TEST: Verify keyring is sealed by default (keyring_unsealed=0)" local keyring_id keyring_id=$(cat "$WORK_DIR/keyring_id") log_info "Current keyring state (should be empty and sealed):" list_keyring_keys show_keyring_status generate_keys # Try to add a key - should fail if keyring is sealed log_info "Attempting to add key to sealed keyring..." if keyctl padd asymmetric "dm-verity-test" "$keyring_id" \ < "$WORK_DIR/cert.der" 2>/dev/null; then log_fail "Keyring should be sealed when keyring_unsealed=0" list_keyring_keys return 1 else log_pass "Keyring is correctly sealed when keyring_unsealed=0" log_info "Keyring state after failed add attempt:" list_keyring_keys return 0 fi } # # Test: Verify dm-verity keyring is inactive when sealed empty # test_keyring_inactive_when_empty() { log_info "TEST: Verify dm-verity keyring is inactive when sealed empty" # When keyring_unsealed=0, the keyring is sealed immediately while empty # This means it should NOT be used for verification (nr_leaves_on_tree=0) log_info "Keyring state (should be empty and sealed):" list_keyring_keys show_keyring_status create_test_device create_verity_hash # Without any keys in the dm-verity keyring, and with it sealed, # verification should fall through to the secondary/platform keyrings # and likely succeed (if require_signatures=0) or fail (if =1) log_info "Sub-test: Device activation with sealed empty keyring" if [ "$REQUIRE_SIGNATURES" = "Y" ] || [ "$REQUIRE_SIGNATURES" = "1" ]; then if activate_verity_device "no"; then log_fail "Device should NOT activate without signature when require_signatures=1" deactivate_verity_device return 1 else log_pass "Device correctly rejected (require_signatures=1, no valid signature)" fi else if activate_verity_device "no"; then log_pass "Device activated (require_signatures=0, empty dm-verity keyring is inactive)" deactivate_verity_device else log_fail "Device should activate when require_signatures=0" return 1 fi fi return 0 } main() { local rc=0 log_info "=== dm-verity keyring test ===" log_info "" # Create work directory WORK_DIR=$(mktemp -d -t dm-verity-test.XXXXXX) log_info "Work directory: $WORK_DIR" check_requirements # # Test 1: UNSEALED keyring mode (keyring_unsealed=1) # log_info "" log_info "========================================" log_info "=== TEST MODE: UNSEALED KEYRING ===" log_info "========================================" log_info "" load_dm_verity_module 1 1 # keyring_unsealed=1, require_signatures=1 show_keyring_status log_info "" if ! test_multiple_keys; then rc=1 fi # After sealing, verify it rejects new keys log_info "" if ! test_sealed_keyring_rejects_keys; then rc=1 fi # Test corrupted signatures are rejected log_info "" if ! test_corrupted_signature; then rc=1 fi # Clean up devices before reloading module deactivate_verity_device if [ -n "$DATA_DEV" ] && [[ "$DATA_DEV" == /dev/loop* ]]; then losetup -d "$DATA_DEV" 2>/dev/null || true DATA_DEV="" fi if [ -n "$HASH_DEV" ] && [[ "$HASH_DEV" == /dev/loop* ]]; then losetup -d "$HASH_DEV" 2>/dev/null || true HASH_DEV="" fi # # Test 2: SEALED keyring mode (keyring_unsealed=0, default) # log_info "" log_info "========================================" log_info "=== TEST MODE: SEALED KEYRING (default) ===" log_info "========================================" log_info "" load_dm_verity_module 0 0 # keyring_unsealed=0, require_signatures=0 show_keyring_status log_info "" if ! test_keyring_sealed_by_default; then rc=1 fi log_info "" if ! test_keyring_inactive_when_empty; then rc=1 fi # # Summary # log_info "" log_info "========================================" if [ $rc -eq 0 ]; then log_info "=== All tests PASSED ===" else log_error "=== Some tests FAILED ===" fi log_info "========================================" return $rc } main "$@"