generic: test adding filesystem-level fscrypt key via key_id
[xfstests-dev.git] / common / encrypt
index b967c65a26f6598d8561fecb4a3abe6c49964868..5695a12307e64237ecb75614bba8e7260e207fc0 100644 (file)
@@ -6,7 +6,7 @@
 
 #
 # _require_scratch_encryption [-c CONTENTS_MODE] [-n FILENAMES_MODE]
-#                            [-v POLICY_VERSION]
+#                            [-f POLICY_FLAGS] [-v POLICY_VERSION]
 #
 # Require encryption support on the scratch device.
 #
@@ -69,15 +69,20 @@ _require_encryption_policy_support()
        local mnt=$1
        local dir=$mnt/tmpdir
        local set_encpolicy_args=""
+       local policy_flags=0
        local policy_version=1
        local c
 
        OPTIND=2
-       while getopts "c:n:v:" c; do
+       while getopts "c:n:f:v:" c; do
                case $c in
                c|n)
                        set_encpolicy_args+=" -$c $OPTARG"
                        ;;
+               f)
+                       set_encpolicy_args+=" -$c $OPTARG"
+                       policy_flags=$OPTARG
+                       ;;
                v)
                        set_encpolicy_args+=" -$c $OPTARG"
                        policy_version=$OPTARG
@@ -92,6 +97,12 @@ _require_encryption_policy_support()
        echo "Checking whether kernel supports encryption policy: $set_encpolicy_args" \
                >> $seqres.full
 
+       if (( policy_flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 )); then
+               _scratch_unmount
+               _scratch_mkfs_stable_inodes_encrypted &>> $seqres.full
+               _scratch_mount
+       fi
+
        mkdir $dir
        if (( policy_version > 1 )); then
                _require_xfs_io_command "get_encpolicy" "-t"
@@ -159,6 +170,34 @@ _scratch_mkfs_sized_encrypted()
        esac
 }
 
+# Like _scratch_mkfs_encrypted(), but add -O stable_inodes if applicable for the
+# filesystem type.  This is necessary for using encryption policies that include
+# the inode number in the IVs, e.g. policies with the IV_INO_LBLK_64 flag set.
+_scratch_mkfs_stable_inodes_encrypted()
+{
+       case $FSTYP in
+       ext4)
+               if ! _scratch_mkfs -O encrypt -O stable_inodes; then
+                       _notrun "-O stable_inodes is not supported"
+               fi
+               ;;
+       *)
+               _scratch_mkfs_encrypted
+               ;;
+       esac
+}
+
+# For some tests it's helpful to always use the same key so that the test's
+# output is always the same.  For this purpose the following key can be used:
+TEST_RAW_KEY=
+for i in {1..64}; do
+       TEST_RAW_KEY+="\\x$(printf "%02x" $i)"
+done
+# Key descriptor: arbitrary value
+TEST_KEY_DESCRIPTOR="0000111122223333"
+# Key identifier: HKDF-SHA512(key=$TEST_RAW_KEY, salt="", info="fscrypt\0\x01")
+TEST_KEY_IDENTIFIER="69b2f6edeee720cce0577937eb8a6751"
+
 # Give the invoking shell a new session keyring.  This makes any keys we add to
 # the session keyring scoped to the lifetime of the test script.
 _new_session_keyring()
@@ -188,6 +227,28 @@ _generate_raw_encryption_key()
        echo $raw
 }
 
+# Serialize an integer into a CPU-endian bytestring of the given length, and
+# print it as a string where each byte is hex-escaped.  For example,
+# `_num_to_hex 1000 4` == "\xe8\x03\x00\x00" if the CPU is little endian.
+_num_to_hex()
+{
+       local value=$1
+       local nbytes=$2
+       local i
+       local big_endian=$(echo -ne '\x11' | od -tx2 | head -1 | \
+                          cut -f2 -d' ' | cut -c1)
+
+       if (( big_endian )); then
+               for (( i = 0; i < nbytes; i++ )); do
+                       printf '\\x%02x' $(((value >> (8*(nbytes-1-i))) & 0xff))
+               done
+       else
+               for (( i = 0; i < nbytes; i++ )); do
+                       printf '\\x%02x' $(((value >> (8*i)) & 0xff))
+               done
+       fi
+}
+
 # Add the specified raw encryption key to the session keyring, using the
 # specified key descriptor.
 _add_session_encryption_key()
@@ -198,12 +259,12 @@ _add_session_encryption_key()
        #
        # Add the key to the session keyring.  The required structure is:
        #
-       #       #define FS_MAX_KEY_SIZE 64
+       #       #define FSCRYPT_MAX_KEY_SIZE 64
        #       struct fscrypt_key {
-       #               u32 mode;
-       #               u8 raw[FS_MAX_KEY_SIZE];
-       #               u32 size;
-       #       } __packed;
+       #               __u32 mode;
+       #               __u8 raw[FSCRYPT_MAX_KEY_SIZE];
+       #               __u32 size;
+       #       };
        #
        # The kernel ignores 'mode' but requires that 'size' be 64.
        #
@@ -214,15 +275,8 @@ _add_session_encryption_key()
        # nice to use the common key prefix, but for now use the filesystem-
        # specific prefix to make it possible to test older kernels...
        #
-       local big_endian=$(echo -ne '\x11' | od -tx2 | head -1 | \
-                          cut -f2 -d' ' | cut -c1 )
-       if (( big_endian )); then
-               local mode='\x00\x00\x00\x00'
-               local size='\x00\x00\x00\x40'
-       else
-               local mode='\x00\x00\x00\x00'
-               local size='\x40\x00\x00\x00'
-       fi
+       local mode=$(_num_to_hex 0 4)
+       local size=$(_num_to_hex 64 4)
        echo -n -e "${mode}${raw}${size}" |
                $KEYCTL_PROG padd logon $FSTYP:$keydesc @s >>$seqres.full
 }
@@ -350,6 +404,44 @@ _user_do_enckey_status()
        _user_do "$XFS_IO_PROG -c \"enckey_status $* $keyspec\" \"$mnt\""
 }
 
+# Require support for adding a key to a filesystem's fscrypt keyring via an
+# "fscrypt-provisioning" keyring key.
+_require_add_enckey_by_key_id()
+{
+       local mnt=$1
+
+       # Userspace support
+       _require_xfs_io_command "add_enckey" "-k"
+
+       # Kernel support
+       if $XFS_IO_PROG -c "add_enckey -k 12345" "$mnt" \
+               |& grep -q 'Invalid argument'; then
+               _notrun "Kernel doesn't support key_id parameter to FS_IOC_ADD_ENCRYPTION_KEY"
+       fi
+}
+
+# Add a key of type "fscrypt-provisioning" to the session keyring and print the
+# resulting key ID.
+_add_fscrypt_provisioning_key()
+{
+       local desc=$1
+       local type=$2
+       local raw=$3
+
+       # The format of the key payload must be:
+       #
+       #       struct fscrypt_provisioning_key_payload {
+       #               __u32 type;
+       #               __u32 __reserved;
+       #               __u8 raw[];
+       #       };
+       #
+       local type_hex=$(_num_to_hex $type 4)
+       local reserved=$(_num_to_hex 0 4)
+       echo -n -e "${type_hex}${reserved}${raw}" |
+               $KEYCTL_PROG padd fscrypt-provisioning "$desc" @s
+}
+
 # Retrieve the encryption nonce of the given inode as a hex string.  The nonce
 # was randomly generated by the filesystem and isn't exposed directly to
 # userspace.  But it can be read using the filesystem's debugging tools.
@@ -568,7 +660,8 @@ _do_verify_ciphertext_for_encryption_policy()
        local set_encpolicy_args=$4
        local keyspec=$5
        local raw_key_hex=$6
-       local crypt_cmd="$here/src/fscrypt-crypt-util $7"
+       local crypt_contents_cmd="$here/src/fscrypt-crypt-util $7"
+       local crypt_filename_cmd="$here/src/fscrypt-crypt-util $8"
 
        local blocksize=$(_get_block_size $SCRATCH_MNT)
        local test_contents_files=()
@@ -626,14 +719,15 @@ _do_verify_ciphertext_for_encryption_policy()
                read -r src inode blocklist <<< "$f"
                nonce=$(_get_encryption_nonce $SCRATCH_DEV $inode)
                _dump_ciphertext_blocks $SCRATCH_DEV $blocklist > $tmp.actual_contents
-               $crypt_cmd $contents_encryption_mode $raw_key_hex \
+               $crypt_contents_cmd $contents_encryption_mode $raw_key_hex \
                        --file-nonce=$nonce --block-size=$blocksize \
-                       < $src > $tmp.expected_contents
+                       --inode-number=$inode < $src > $tmp.expected_contents
                if ! cmp $tmp.expected_contents $tmp.actual_contents; then
                        _fail "Expected encrypted contents != actual encrypted contents.  File: $f"
                fi
-               $crypt_cmd $contents_encryption_mode $raw_key_hex --decrypt \
-                       --file-nonce=$nonce --block-size=$blocksize \
+               $crypt_contents_cmd $contents_encryption_mode $raw_key_hex \
+                       --decrypt --file-nonce=$nonce --block-size=$blocksize \
+                       --inode-number=$inode \
                        < $tmp.actual_contents > $tmp.decrypted_contents
                if ! cmp $src $tmp.decrypted_contents; then
                        _fail "Contents decryption sanity check failed.  File: $f"
@@ -647,16 +741,17 @@ _do_verify_ciphertext_for_encryption_policy()
                _get_ciphertext_filename $SCRATCH_DEV $inode $dir_inode \
                        > $tmp.actual_name
                echo -n "$name" | \
-                       $crypt_cmd $filenames_encryption_mode $raw_key_hex \
-                       --file-nonce=$nonce --padding=$padding \
-                       --block-size=255 > $tmp.expected_name
+                       $crypt_filename_cmd $filenames_encryption_mode \
+                       $raw_key_hex --file-nonce=$nonce --padding=$padding \
+                       --block-size=255 --inode-number=$dir_inode \
+                       > $tmp.expected_name
                if ! cmp $tmp.expected_name $tmp.actual_name; then
                        _fail "Expected encrypted filename != actual encrypted filename.  File: $f"
                fi
-               $crypt_cmd $filenames_encryption_mode $raw_key_hex --decrypt \
-                       --file-nonce=$nonce --padding=$padding \
-                       --block-size=255 < $tmp.actual_name \
-                       > $tmp.decrypted_name
+               $crypt_filename_cmd $filenames_encryption_mode $raw_key_hex \
+                       --decrypt --file-nonce=$nonce --padding=$padding \
+                       --block-size=255 --inode-number=$dir_inode \
+                       < $tmp.actual_name > $tmp.decrypted_name
                decrypted_name=$(tr -d '\0' < $tmp.decrypted_name)
                if [ "$name" != "$decrypted_name" ]; then
                        _fail "Filename decryption sanity check failed ($name != $decrypted_name).  File: $f"
@@ -673,6 +768,10 @@ FSCRYPT_MODE_AES_128_CTS=6
 FSCRYPT_MODE_ADIANTUM=9
 
 FSCRYPT_POLICY_FLAG_DIRECT_KEY=0x04
+FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64=0x08
+
+FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR=1
+FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER=2
 
 _fscrypt_mode_name_to_num()
 {
@@ -692,8 +791,13 @@ _fscrypt_mode_name_to_num()
 # policy of the specified type is used.
 #
 # The first two parameters are the contents and filenames encryption modes to
-# test.  Optionally, also specify 'direct' to test the DIRECT_KEY flag, and/or
-# 'v2' to test v2 policies.
+# test.  The following optional parameters are also accepted to further modify
+# the type of encryption policy that is tested:
+#
+#      'v2':                   test a v2 encryption policy
+#      'direct':               test the DIRECT_KEY policy flag
+#      'iv_ino_lblk_64':       test the IV_INO_LBLK_64 policy flag
+#
 _verify_ciphertext_for_encryption_policy()
 {
        local contents_encryption_mode=$1
@@ -703,6 +807,8 @@ _verify_ciphertext_for_encryption_policy()
        local policy_flags=0
        local set_encpolicy_args=""
        local crypt_util_args=""
+       local crypt_util_contents_args=""
+       local crypt_util_filename_args=""
 
        shift 2
        for opt; do
@@ -717,6 +823,9 @@ _verify_ciphertext_for_encryption_policy()
                        fi
                        (( policy_flags |= FSCRYPT_POLICY_FLAG_DIRECT_KEY ))
                        ;;
+               iv_ino_lblk_64)
+                       (( policy_flags |= FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 ))
+                       ;;
                *)
                        _fail "Unknown option '$opt' passed to ${FUNCNAME[0]}"
                        ;;
@@ -732,9 +841,19 @@ _verify_ciphertext_for_encryption_policy()
                set_encpolicy_args+=" -v 2"
                crypt_util_args+=" --kdf=HKDF-SHA512"
                if (( policy_flags & FSCRYPT_POLICY_FLAG_DIRECT_KEY )); then
+                       if (( policy_flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 )); then
+                               _fail "'direct' and 'iv_ino_lblk_64' options are mutually exclusive"
+                       fi
                        crypt_util_args+=" --mode-num=$contents_mode_num"
+               elif (( policy_flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 )); then
+                       crypt_util_args+=" --iv-ino-lblk-64"
+                       crypt_util_contents_args+=" --mode-num=$contents_mode_num"
+                       crypt_util_filename_args+=" --mode-num=$filenames_mode_num"
                fi
        else
+               if (( policy_flags & ~FSCRYPT_POLICY_FLAG_DIRECT_KEY )); then
+                       _fail "unsupported flags for v1 policy: $policy_flags"
+               fi
                if (( policy_flags & FSCRYPT_POLICY_FLAG_DIRECT_KEY )); then
                        crypt_util_args+=" --kdf=none"
                else
@@ -743,7 +862,7 @@ _verify_ciphertext_for_encryption_policy()
        fi
        set_encpolicy_args=${set_encpolicy_args# }
 
-       _require_scratch_encryption $set_encpolicy_args
+       _require_scratch_encryption $set_encpolicy_args -f $policy_flags
        _require_test_program "fscrypt-crypt-util"
        _require_xfs_io_command "fiemap"
        _require_get_encryption_nonce_support
@@ -753,9 +872,18 @@ _verify_ciphertext_for_encryption_policy()
        fi
 
        echo "Creating encryption-capable filesystem" >> $seqres.full
-       _scratch_mkfs_encrypted &>> $seqres.full
+       if (( policy_flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 )); then
+               _scratch_mkfs_stable_inodes_encrypted &>> $seqres.full
+       else
+               _scratch_mkfs_encrypted &>> $seqres.full
+       fi
        _scratch_mount
 
+       crypt_util_args+=" --fs-uuid=$(blkid -s UUID -o value $SCRATCH_DEV | tr -d -)"
+
+       crypt_util_contents_args+="$crypt_util_args"
+       crypt_util_filename_args+="$crypt_util_args"
+
        echo "Generating encryption key" >> $seqres.full
        local raw_key=$(_generate_raw_encryption_key)
        if (( policy_version > 1 )); then
@@ -781,5 +909,6 @@ _verify_ciphertext_for_encryption_policy()
                "$set_encpolicy_args" \
                "$keyspec" \
                "$raw_key_hex" \
-               "$crypt_util_args"
+               "$crypt_util_contents_args" \
+               "$crypt_util_filename_args"
 }