On 01/05/2020 07:39, Eric Biggers wrote:
[...]
Thanks for taking the time to look through this.
>
> This is a good idea, but can you explain exactly what security properties you
> aim to achieve?
My goal is to protect the file-system against offline modifications.
Offline in this context means when the filesystem is not mounted.
This could be a switched off laptop in a hotel room or a container
image, or a powered off embedded system. When the file-system is mounted
normal read/write access is possible.
> As far as I can tell, btrfs hashes each data block individually. There's no
> contextual information about where eaech block is located or which file(s) it
> belongs to. So, with this proposal, an attacker can still replace any data
> block with any other data block. Is that what you have in mind? Have you
> considered including contextual information in the hashes, to prevent this?
>
> What about metadata blocks -- how well are they authenticated? Can they be
> moved around? And does this proposal authenticate *everything* on the
> filesystem, or is there any part that is missed? What about the superblock?
In btrfs every metadata block is started by a checksum (see struct
btrfs_header or struct btrfs_super_block). This checksum protects the
whole meta-data block (minus the checksum field, obviously).
The two main structure of the trees are btrfs_node and btrfs_leaf (both
started by a btrfs_header). struct btrfs_node holds the generation and
the block pointers of child nodes (and leafs). Struct btrfs_leaf holds
the items, which can be inodes, directories, extents, checksums,
block-groups, etc...
As each FS meta-data item, beginning with the super block, downwards to
the meta-data items themselves is protected be a checksum, so the FS
tree (including locations, generation, etc) is protected by a checksum,
for which the attacker would need to know the key to generate.
The checksum for data blocks is saved in a separate on-disk btree, the
checksum tree. The structure of the checksum tree consists of
btrfs_leafs and btrfs_nodes as well, both of which do have a
btrfs_header and thus are protected by the checksums.
>
> You also mentioned preventing replay of filesystem operations, which suggests
> you're trying to achieve rollback protection. But actually this scheme doesn't
> provide rollback protection. For one, an attacker can always just rollback the
> entire filesystem to a previous state.
You're right, replay is the wrong wording there and it's actually
harmful in the used context. What I had in mind was, in order to change
the structure of the filesystem, an attacker would need the key to
update the checksums, otherwise the next read will detect a corruption.
As for a real replay case, an attacker would need to increase the
generation of the tree block, in order to roll back to a older state, an
attacker still would need to modify the super-block's generation, which
is protected by the checksum as well.
> This feature would still be useful even with the above limitations. But what is
> your goal exactly? Can this be made better?
>
>> diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
>> index d10c7be10f3b..fe403fb62178 100644
>> --- a/fs/btrfs/disk-io.c
>> +++ b/fs/btrfs/disk-io.c
>> @@ -17,6 +17,7 @@
>> #include <linux/error-injection.h>
>> #include <linux/crc32c.h>
>> #include <linux/sched/mm.h>
>> +#include <keys/user-type.h>
>> #include <asm/unaligned.h>
>> #include <crypto/hash.h>
>> #include "ctree.h"
>> @@ -339,6 +340,7 @@ static bool btrfs_supported_super_csum(u16 csum_type)
>> case BTRFS_CSUM_TYPE_XXHASH:
>> case BTRFS_CSUM_TYPE_SHA256:
>> case BTRFS_CSUM_TYPE_BLAKE2:
>> + case BTRFS_CSUM_TYPE_HMAC_SHA256:
>> return true;
>> default:
>> return false;
>> @@ -2187,6 +2189,9 @@ static int btrfs_init_csum_hash(struct btrfs_fs_info *fs_info, u16 csum_type)
>> {
>> struct crypto_shash *csum_shash;
>> const char *csum_driver = btrfs_super_csum_driver(csum_type);
>> + struct key *key;
>> + const struct user_key_payload *ukp;
>> + int err = 0;
>>
>> csum_shash = crypto_alloc_shash(csum_driver, 0, 0);
>>
>> @@ -2198,7 +2203,53 @@ static int btrfs_init_csum_hash(struct btrfs_fs_info *fs_info, u16 csum_type)
>>
>> fs_info->csum_shash = csum_shash;
>>
>> - return 0;
>> + /*
>> + * if we're not doing authentication, we're done by now. Still we have
>> + * to validate the possible combinations of BTRFS_MOUNT_AUTH_KEY and
>> + * keyed hashes.
>> + */
>> + if (csum_type == BTRFS_CSUM_TYPE_HMAC_SHA256 &&
>> + !btrfs_test_opt(fs_info, AUTH_KEY)) {
>> + crypto_free_shash(fs_info->csum_shash);
>> + return -EINVAL;
>
> Seems there should be an error message here that says that a key is needed.
>
>> + } else if (btrfs_test_opt(fs_info, AUTH_KEY)
>> + && csum_type != BTRFS_CSUM_TYPE_HMAC_SHA256) {
>> + crypto_free_shash(fs_info->csum_shash);
>> + return -EINVAL;
>
> The hash algorithm needs to be passed as a mount option. Otherwise the attacker
> gets to choose it for you among all the supported keyed hash algorithms, as soon
> as support for a second one is added. Maybe use 'auth_hash_name' like UBIFS
> does?
Can you elaborate a bit more on that? As far as I know, UBIFS doesn't
save the 'auth_hash_name' on disk, whereas 'BTRFS_CSUM_TYPE_HMAC_SHA256'
is part of the on-disk format. As soon as we add a 2nd keyed hash, say
BTRFS_CSUM_TYPE_BLAKE2B_KEYED, this will be in the superblock as well,
as struct btrfs_super_block::csum_type.
>
>> + } else if (!btrfs_test_opt(fs_info, AUTH_KEY)) {
>> + /*
>> + * This is the normal case, if noone want's authentication and
>> + * doesn't have a keyed hash, we're done.
>> + */
>> + return 0;
>> + }
>> +
>> + key = request_key(&key_type_logon, fs_info->auth_key_name, NULL);
>> + if (IS_ERR(key))
>> + return PTR_ERR(key);
>
> Seems this should print an error message if the key can't be found.
>
> Also, this should enforce that the key description uses a service prefix by
> formatting it as kasprintf("btrfs:%s", fs_info->auth_key_name). Otherwise
> people can abuse this feature to use keys that were added for other kernel
> features. (This already got screwed up elsewhere, which makes the "logon" key
> type a bit of a disaster. But there's no need to make it worse.)
OK, will do.