#! /bin/bash
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2018 Google, Inc.  All Rights Reserved.
#
# FS QA Test generic/902
#
# Test fs-verity descriptor validation.
#
seq=`basename $0`
seqres=$RESULT_DIR/$seq
echo "QA output created by $seq"

here=`pwd`
tmp=/tmp/$$
status=1	# failure is the default!
trap "_cleanup; exit \$status" 0 1 2 3 15

_cleanup()
{
	cd /
	rm -f $tmp.*
}

# get standard environment, filters and checks
. ./common/rc
. ./common/filter
. ./common/verity

# remove previous $seqres.full before test
rm -f $seqres.full

# real QA test starts here
_supported_fs generic
_supported_os Linux
_require_scratch_verity

_scratch_mkfs_verity &>> $seqres.full
_scratch_mount
fsv_orig_file=$SCRATCH_MNT/file
fsv_file=$SCRATCH_MNT/file.fsv

# Serialize an integer into a little endian hex bytestring of the given length,
# e.g. `num_to_hex 1000 4` == "\xe8\x03\x00\x00"
num_to_hex()
{
	local value=$1
	local nbytes=$2
	local i

	for (( i = 0; i < nbytes; i++, value >>= 8 )); do
		printf '\\x%02x' $((value & 0xff))
	done
}

# Number of bytes in a hex bytestring, e.g. `hexstr_len "\xe8\x03"` == 2
hexstr_len()
{
	echo -n -e "$1" | wc -c
}

# Get a file's SHA-256 digest as a hex bytestring for "echo -e"
do_sha256sum()
{
	sha256sum "$@" | awk '{print $1}' | sed 's/../\\x\0/g'
}

# Append a field to 'str', allowing override commands.  See make_test_file().
do_add_field()
{
	local fixed_length=$1
	local -n str=$2
	local fieldname=$3
	local default_val=$4
	shift 4
	local cmds=("$@")
	local cmd
	local val="$default_val"

	for cmd in "${cmds[@]}"; do
		if [ $(echo "$cmd" | cut -d' ' -f1) = $fieldname ]; then
			val=$(echo "$cmd" | sed "s/^$fieldname *//")
			if $fixed_length && \
			  [ $(hexstr_len "$val") != $(hexstr_len "$default_val") ]
			then
				_fail "wrong value length in '$cmd'"
			fi
			break
		fi
	done
	str+="$val"
}

add_field()
{
	do_add_field true "$@"
}

add_varfield()
{
	do_add_field false "$@"
}

FS_VERITY_EXT_ROOT_HASH=1
FS_VERITY_EXT_SALT=2
FS_VERITY_EXT_PKCS7_SIGNATURE=3
FS_VERITY_EXT_ELIDE=4
FS_VERITY_EXT_PATCH=5

EXTHDR_SIZE=8

# Create an extension header (struct fsverity_extension)
create_exthdr()
{
	local length=$1
	local type=$2
	if [ $# -ge 3 ]; then
		local reserved=$3
	else
		local reserved=0
	fi

	num_to_hex $length 4
	num_to_hex $type 2
	num_to_hex $reserved 2
}

# Create an extension item, given the type and payload
create_ext()
{
	local type=$1
	local payload=$2
	local payload_size=$(hexstr_len "$payload")

	create_exthdr $(( EXTHDR_SIZE + payload_size )) $type
	echo -n "$payload"
	num_to_hex 0 $(( -payload_size & 7 ))
}

# Create a ROOT_HASH extension item
create_root_hash_ext()
{
	local root_hash=$1

	create_ext $FS_VERITY_EXT_ROOT_HASH "$root_hash"
}

DEFAULT_AUTH_EXT_COUNT=1	# root hash
DEFAULT_UNAUTH_EXT_COUNT=0	# none

#
# Generate a file and append fs-verity metadata to it, allowing metadata fields
# to be overridden.  The overrides are given as command strings in the format
# "$field $value".  E.g., "major_version \x01" sets the major_version field to
# the byte \x01 (binary 1, not ASCII 1).  For fixed-length fields (add_field())
# the override must be the same length as the default value; for variable-length
# fields (add_varfield()) the override can be any length.
#
make_test_file()
{
	local cmds=("$@")
	local out=""

	# 8 KiB file
	head -c 8192 /dev/urandom > $fsv_orig_file
	cp $fsv_orig_file $fsv_file

	# Generate the Merkle tree.. there are just 2 data blocks, so it's easy.
	local hash1=$(head -c 4096 $fsv_file | do_sha256sum)
	local hash2=$(tail -c 4096 $fsv_file | do_sha256sum)
	echo -n -e "$hash1" >> $fsv_file
	echo -n -e "$hash2" >> $fsv_file
	head -c $((4096 - (32*2))) /dev/zero >> $fsv_file
	local root_hash=$(tail -c 4096 $fsv_file | do_sha256sum)

	# Append the 'struct fsverity_descriptor'
	add_field out magic "FSVerity" "${cmds[@]}"
	add_field out major_version "\x01" "${cmds[@]}"
	add_field out minor_version "\x00" "${cmds[@]}"
	add_field out log_data_blocksize "$(num_to_hex 12 1)" "${cmds[@]}" # 4K block size
	add_field out log_tree_blocksize "$(num_to_hex 12 1)" "${cmds[@]}"
	add_field out data_algorithm "$(num_to_hex 1 2)" "${cmds[@]}" # SHA-256
	add_field out tree_algorithm "$(num_to_hex 1 2)" "${cmds[@]}"
	add_field out flags "$(num_to_hex 0 4)" "${cmds[@]}"
	add_field out reserved1 "$(num_to_hex 0 4)" "${cmds[@]}"
	add_field out orig_file_size "$(num_to_hex 8192 8)" "${cmds[@]}"
	add_field out auth_ext_count "$(num_to_hex $DEFAULT_AUTH_EXT_COUNT 2)" "${cmds[@]}"
	add_field out reserved2 "$(num_to_hex 0 30)" "${cmds[@]}"

	# Append the authenticated extensions (default: just the root hash)
	add_varfield out root_hash "$(create_root_hash_ext "$root_hash")" "${cmds[@]}"
	add_varfield out auth_extensions "" "${cmds[@]}"

	# Append the unauthenticated extensions (default: none)
	add_field out unauth_ext_count "$(num_to_hex $DEFAULT_UNAUTH_EXT_COUNT 2)" "${cmds[@]}"
	add_field out unauth_ext_count_padding "$(num_to_hex 0 6)" "${cmds[@]}"
	add_varfield out unauth_extensions "" "${cmds[@]}"

	# No gap before the footer by default
	add_varfield out gap_before_footer "" "${cmds[@]}"

	# Append the footer
	local desc_reverse_offset=$((12 + $(hexstr_len "$out") ))
	add_field out desc_reverse_offset "$(num_to_hex $desc_reverse_offset 4)" "${cmds[@]}"
	add_field out ftr_magic "FSVerity" "${cmds[@]}"

	echo -n -e "$out" >> $fsv_file
}

desc_test()
{
	local description=$1
	shift
	local cmds=("$@")

	_fsv_begin_subtest "$description"
	make_test_file "${cmds[@]}"
	{
		if _fsv_enable $fsv_file; then
			cmp $fsv_file $fsv_orig_file
		fi
	} |& _filter_scratch
}

ext_count()
{
	local type=$1
	local count=$2
	local default_count=$3
	local sign=${count:0:1}
	if [ $sign = '+' ] || [ $sign = '-' ]; then
		count=$(( default_count + $count ))
	fi
	echo "$type $(num_to_hex $count 2)"
}

auth_ext_count()
{
	ext_count "auth_ext_count" "$1" $DEFAULT_AUTH_EXT_COUNT
}

unauth_ext_count()
{
	ext_count "unauth_ext_count" "$1" $DEFAULT_UNAUTH_EXT_COUNT
}

desc_test "control case, valid file"
desc_test "multiple pages, valid file" "gap_before_footer $(num_to_hex 0 10000)"

desc_test "bad magic: XXXXXXXX" "magic XXXXXXXX"
desc_test "bad magic: FSVeritY" "magic FSVeritY"
desc_test "bad major_version" "major_version \xff"
desc_test "bad minor_version" "minor_version \xff"
desc_test "bad log_data_blocksize: 0x00" "log_data_blocksize \x00"
desc_test "bad log_data_blocksize: 0xff" "log_data_blocksize \xff"
desc_test "bad log_tree_blocksize: 0x00" "log_tree_blocksize \x00"
desc_test "bad log_tree_blocksize: 0xff" "log_tree_blocksize \xff"
desc_test "bad data_algorithm: 0x0000" "data_algorithm \x00\x00"
desc_test "bad data_algorithm: 0xffff" "data_algorithm \xff\xff"
desc_test "bad tree_algorithm: 0x0000" "tree_algorithm \x00\x00"
desc_test "bad tree_algorithm: 0xffff" "tree_algorithm \xff\xff"
desc_test "mismatched block sizes" "log_data_blocksize \x10" "log_tree_blocksize \x0C"
desc_test "mismatched algorithms" "data_algorithm \x01\x00" "tree_algorithm \x02\x00"
desc_test "bad flags" "flags \xff\xff\xff\xff"
desc_test "bad reserved1" "reserved1 \xff\xff\xff\xff"
desc_test "bad orig_file_size: 0" "orig_file_size $(num_to_hex 0 8)"
desc_test "bad orig_file_size: > full_isize" "orig_file_size $(num_to_hex 100000 8)"
desc_test "bad orig_file_size: UINT64_MAX" "orig_file_size \xff\xff\xff\xff\xff\xff\xff\xff"
desc_test "bad auth_ext_count" "$(auth_ext_count 65535)"
desc_test "bad reserved2" "reserved2 $(perl -e 'print "\\xff" x 30')"

desc_test "bad desc_reverse_offset: 0" "desc_reverse_offset $(num_to_hex 0 4)"
desc_test "bad desc_reverse_offset: 64" "desc_reverse_offset $(num_to_hex 64 4)"
desc_test "bad desc_reverse_offset: 69" "desc_reverse_offset $(num_to_hex 69 4)"
desc_test "bad desc_reverse_offset: > full_isize" "desc_reverse_offset $(num_to_hex 100000 4)"
desc_test "bad desc_reverse_offset: UINT32_MAX" "desc_reverse_offset \xff\xff\xff\xff"
desc_test "bad ftr_magic: XXXXXXXX" "ftr_magic XXXXXXXX"
desc_test "bad ftr_magic: FSVeritY" "ftr_magic FSVeritY"

desc_test "root hash length wrong: 0" \
	"root_hash $(create_root_hash_ext "$(num_to_hex 0 0)")"
desc_test "root hash length wrong: too short" \
	"root_hash $(create_root_hash_ext "$(num_to_hex 0 16)")"
desc_test "root hash length wrong: too long" \
	"root_hash $(create_root_hash_ext "$(num_to_hex 0 100)")"
desc_test "multiple root hashes" "$(auth_ext_count +1)" \
	"auth_extensions $(create_root_hash_ext "$(num_to_hex 0 32)")"
desc_test "no root hash" "$(auth_ext_count -1)" "root_hash"

desc_test "root hash is unauthenticated" \
	"$(auth_ext_count -1)" \
	"$(unauth_ext_count +1)" \
	"root_hash" \
	"unauth_extensions $(create_root_hash_ext $(num_to_hex 0 32))"

desc_test "salt is unauthenticated" \
	"$(unauth_ext_count +1)" \
	"unauth_extensions $(create_ext $FS_VERITY_EXT_SALT foo)"

desc_test "unknown extension type" \
	"$(auth_ext_count +1)" \
	"auth_extensions $(create_ext 255 "")"

desc_test "length in extension header smaller than header" \
	"$(auth_ext_count +1)" \
	"auth_extensions $(create_exthdr 0 $FS_VERITY_EXT_SALT)"

desc_test "extension length overflows buffer" \
	"$(auth_ext_count +1)" \
	"auth_extensions $(create_exthdr 50000 $FS_VERITY_EXT_SALT)"

desc_test "extension length wraps to 0 after rounding" \
	"$(auth_ext_count +1)" \
	"auth_extensions $(create_exthdr 0xffffffff $FS_VERITY_EXT_SALT)"

desc_test "reserved bits set in extension header" \
	"$(auth_ext_count +1)" \
	"auth_extensions $(create_exthdr $EXTHDR_SIZE $FS_VERITY_EXT_SALT 1000)"

# success, all done
status=0
exit
