From 163f3d3a1fc144f1b9b358cec616d4cba4e87d67 Mon Sep 17 00:00:00 2001 From: Rob N Date: Fri, 3 Mar 2023 08:39:09 +1100 Subject: [PATCH] zdb: add decryption support The approach is straightforward: for dataset ops, if a key was offered, find the encryption root and the various encryption parameters, derive a wrapping key if necessary, and then unlock the encryption root. After that all the regular dataset ops will return unencrypted data, and that's kinda the whole thing. Reviewed-by: Brian Behlendorf Reviewed-by: Jorgen Lundman Signed-off-by: Rob Norris Closes #11551 Closes #12707 Closes #14503 --- cmd/zdb/Makefile.am | 3 + cmd/zdb/zdb.c | 174 ++++++++++++++++-- man/man8/zdb.8 | 22 +++ tests/runfiles/common.run | 6 +- tests/zfs-tests/tests/Makefile.am | 1 + .../functional/cli_root/zdb/zdb_args_neg.ksh | 2 +- .../functional/cli_root/zdb/zdb_encrypted.ksh | 69 +++++++ 7 files changed, 262 insertions(+), 15 deletions(-) create mode 100755 tests/zfs-tests/tests/functional/cli_root/zdb/zdb_encrypted.ksh diff --git a/cmd/zdb/Makefile.am b/cmd/zdb/Makefile.am index b80f38b3fd57..c93c9c37cd8d 100644 --- a/cmd/zdb/Makefile.am +++ b/cmd/zdb/Makefile.am @@ -1,4 +1,5 @@ zdb_CPPFLAGS = $(AM_CPPFLAGS) $(FORCEDEBUG_CPPFLAGS) +zdb_CFLAGS = $(AM_CFLAGS) $(LIBCRYPTO_CFLAGS) sbin_PROGRAMS += zdb CPPCHECKTARGETS += zdb @@ -12,3 +13,5 @@ zdb_LDADD = \ libzpool.la \ libzfs_core.la \ libnvpair.la + +zdb_LDADD += $(LIBCRYPTO_LIBS) diff --git a/cmd/zdb/zdb.c b/cmd/zdb/zdb.c index d239da67613c..58bd1da579cd 100644 --- a/cmd/zdb/zdb.c +++ b/cmd/zdb/zdb.c @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -785,16 +786,17 @@ usage(void) "Usage:\t%s [-AbcdDFGhikLMPsvXy] [-e [-V] [-p ...]] " "[-I ]\n" "\t\t[-o =]... [-t ] [-U ] [-x ]\n" + "\t\t[-K ]\n" "\t\t[[/] [ ...]]\n" - "\t%s [-AdiPv] [-e [-V] [-p ...]] [-U ]\n" + "\t%s [-AdiPv] [-e [-V] [-p ...]] [-U ] [-K ]\n" "\t\t[[/] [ ...]\n" "\t%s [-v] \n" "\t%s -C [-A] [-U ]\n" "\t%s -l [-Aqu] \n" "\t%s -m [-AFLPX] [-e [-V] [-p ...]] [-t ] " "[-U ]\n\t\t [ [ ...]]\n" - "\t%s -O \n" - "\t%s -r \n" + "\t%s -O [-K ] \n" + "\t%s -r [-K ] \n" "\t%s -R [-A] [-e [-V] [-p ...]] [-U ]\n" "\t\t ::[:]\n" "\t%s -E [-A] word0:word1:...:word15\n" @@ -879,6 +881,8 @@ usage(void) (void) fprintf(stderr, " -I --inflight=INTEGER " "specify the maximum number of checksumming I/Os " "[default is 200]\n"); + (void) fprintf(stderr, " -K --key=KEY " + "decryption key for encrypted dataset\n"); (void) fprintf(stderr, " -o --option=\"OPTION=INTEGER\" " "set global variable to an unsigned 32-bit integer\n"); (void) fprintf(stderr, " -p --path==PATH " @@ -3023,6 +3027,117 @@ verify_dd_livelist(objset_t *os) return (0); } +static char *key_material = NULL; + +static boolean_t +zdb_derive_key(dsl_dir_t *dd, uint8_t *key_out) +{ + uint64_t keyformat, salt, iters; + int i; + unsigned char c; + + VERIFY0(zap_lookup(dd->dd_pool->dp_meta_objset, dd->dd_crypto_obj, + zfs_prop_to_name(ZFS_PROP_KEYFORMAT), sizeof (uint64_t), + 1, &keyformat)); + + switch (keyformat) { + case ZFS_KEYFORMAT_HEX: + for (i = 0; i < WRAPPING_KEY_LEN * 2; i += 2) { + if (!isxdigit(key_material[i]) || + !isxdigit(key_material[i+1])) + return (B_FALSE); + if (sscanf(&key_material[i], "%02hhx", &c) != 1) + return (B_FALSE); + key_out[i / 2] = c; + } + break; + + case ZFS_KEYFORMAT_PASSPHRASE: + VERIFY0(zap_lookup(dd->dd_pool->dp_meta_objset, + dd->dd_crypto_obj, zfs_prop_to_name(ZFS_PROP_PBKDF2_SALT), + sizeof (uint64_t), 1, &salt)); + VERIFY0(zap_lookup(dd->dd_pool->dp_meta_objset, + dd->dd_crypto_obj, zfs_prop_to_name(ZFS_PROP_PBKDF2_ITERS), + sizeof (uint64_t), 1, &iters)); + + if (PKCS5_PBKDF2_HMAC_SHA1(key_material, strlen(key_material), + ((uint8_t *)&salt), sizeof (uint64_t), iters, + WRAPPING_KEY_LEN, key_out) != 1) + return (B_FALSE); + + break; + + default: + fatal("no support for key format %u\n", + (unsigned int) keyformat); + } + + return (B_TRUE); +} + +static char encroot[ZFS_MAX_DATASET_NAME_LEN]; +static boolean_t key_loaded = B_FALSE; + +static void +zdb_load_key(objset_t *os) +{ + dsl_pool_t *dp; + dsl_dir_t *dd, *rdd; + uint8_t key[WRAPPING_KEY_LEN]; + uint64_t rddobj; + int err; + + dp = spa_get_dsl(os->os_spa); + dd = os->os_dsl_dataset->ds_dir; + + dsl_pool_config_enter(dp, FTAG); + VERIFY0(zap_lookup(dd->dd_pool->dp_meta_objset, dd->dd_crypto_obj, + DSL_CRYPTO_KEY_ROOT_DDOBJ, sizeof (uint64_t), 1, &rddobj)); + VERIFY0(dsl_dir_hold_obj(dd->dd_pool, rddobj, NULL, FTAG, &rdd)); + dsl_dir_name(rdd, encroot); + dsl_dir_rele(rdd, FTAG); + + if (!zdb_derive_key(dd, key)) + fatal("couldn't derive encryption key"); + + dsl_pool_config_exit(dp, FTAG); + + ASSERT3U(dsl_dataset_get_keystatus(dd), ==, ZFS_KEYSTATUS_UNAVAILABLE); + + dsl_crypto_params_t *dcp; + nvlist_t *crypto_args; + + crypto_args = fnvlist_alloc(); + fnvlist_add_uint8_array(crypto_args, "wkeydata", + (uint8_t *)key, WRAPPING_KEY_LEN); + VERIFY0(dsl_crypto_params_create_nvlist(DCP_CMD_NONE, + NULL, crypto_args, &dcp)); + err = spa_keystore_load_wkey(encroot, dcp, B_FALSE); + + dsl_crypto_params_free(dcp, (err != 0)); + fnvlist_free(crypto_args); + + if (err != 0) + fatal( + "couldn't load encryption key for %s: %s", + encroot, strerror(err)); + + ASSERT3U(dsl_dataset_get_keystatus(dd), ==, ZFS_KEYSTATUS_AVAILABLE); + + printf("Unlocked encryption root: %s\n", encroot); + key_loaded = B_TRUE; +} + +static void +zdb_unload_key(void) +{ + if (!key_loaded) + return; + + VERIFY0(spa_keystore_unload_wkey(encroot)); + key_loaded = B_FALSE; +} + static avl_tree_t idx_tree; static avl_tree_t domain_tree; static boolean_t fuid_table_loaded; @@ -3037,12 +3152,36 @@ open_objset(const char *path, const void *tag, objset_t **osp) uint64_t version = 0; VERIFY3P(sa_os, ==, NULL); + /* * We can't own an objset if it's redacted. Therefore, we do this * dance: hold the objset, then acquire a long hold on its dataset, then * release the pool (which is held as part of holding the objset). */ - err = dmu_objset_hold(path, tag, osp); + + if (dump_opt['K']) { + /* decryption requested, try to load keys */ + err = dmu_objset_hold(path, tag, osp); + if (err != 0) { + (void) fprintf(stderr, "failed to hold dataset " + "'%s': %s\n", + path, strerror(err)); + return (err); + } + dsl_dataset_long_hold(dmu_objset_ds(*osp), tag); + dsl_pool_rele(dmu_objset_pool(*osp), tag); + + /* succeeds or dies */ + zdb_load_key(*osp); + + /* release it all */ + dsl_dataset_long_rele(dmu_objset_ds(*osp), tag); + dsl_dataset_rele(dmu_objset_ds(*osp), tag); + } + + int ds_hold_flags = key_loaded ? DS_HOLD_FLAG_DECRYPT : 0; + + err = dmu_objset_hold_flags(path, ds_hold_flags, tag, osp); if (err != 0) { (void) fprintf(stderr, "failed to hold dataset '%s': %s\n", path, strerror(err)); @@ -3051,7 +3190,8 @@ open_objset(const char *path, const void *tag, objset_t **osp) dsl_dataset_long_hold(dmu_objset_ds(*osp), tag); dsl_pool_rele(dmu_objset_pool(*osp), tag); - if (dmu_objset_type(*osp) == DMU_OST_ZFS && !(*osp)->os_encrypted) { + if (dmu_objset_type(*osp) == DMU_OST_ZFS && + (key_loaded || !(*osp)->os_encrypted)) { (void) zap_lookup(*osp, MASTER_NODE_OBJ, ZPL_VERSION_STR, 8, 1, &version); if (version >= ZPL_VERSION_SA) { @@ -3064,7 +3204,8 @@ open_objset(const char *path, const void *tag, objset_t **osp) (void) fprintf(stderr, "sa_setup failed: %s\n", strerror(err)); dsl_dataset_long_rele(dmu_objset_ds(*osp), tag); - dsl_dataset_rele(dmu_objset_ds(*osp), tag); + dsl_dataset_rele_flags(dmu_objset_ds(*osp), + ds_hold_flags, tag); *osp = NULL; } } @@ -3080,9 +3221,12 @@ close_objset(objset_t *os, const void *tag) if (os->os_sa != NULL) sa_tear_down(os); dsl_dataset_long_rele(dmu_objset_ds(os), tag); - dsl_dataset_rele(dmu_objset_ds(os), tag); + dsl_dataset_rele_flags(dmu_objset_ds(os), + key_loaded ? DS_HOLD_FLAG_DECRYPT : 0, tag); sa_attr_table = NULL; sa_os = NULL; + + zdb_unload_key(); } static void @@ -3464,7 +3608,7 @@ dump_object(objset_t *os, uint64_t object, int verbosity, if (error) fatal("dmu_object_info() failed, errno %u", error); - if (os->os_encrypted && + if (!key_loaded && os->os_encrypted && DMU_OT_IS_ENCRYPTED(doi.doi_bonus_type)) { error = dnode_hold(os, object, FTAG, &dn); if (error) @@ -3561,7 +3705,8 @@ dump_object(objset_t *os, uint64_t object, int verbosity, (void) printf("\t\t(bonus encrypted)\n"); } - if (!os->os_encrypted || !DMU_OT_IS_ENCRYPTED(doi.doi_type)) { + if (key_loaded || + (!os->os_encrypted || !DMU_OT_IS_ENCRYPTED(doi.doi_type))) { object_viewer[ZDB_OT_TYPE(doi.doi_type)](os, object, NULL, 0); } else { @@ -8516,6 +8661,7 @@ main(int argc, char **argv) {"intent-logs", no_argument, NULL, 'i'}, {"inflight", required_argument, NULL, 'I'}, {"checkpointed-state", no_argument, NULL, 'k'}, + {"key", required_argument, NULL, 'K'}, {"label", no_argument, NULL, 'l'}, {"disable-leak-tracking", no_argument, NULL, 'L'}, {"metaslabs", no_argument, NULL, 'm'}, @@ -8544,7 +8690,7 @@ main(int argc, char **argv) }; while ((c = getopt_long(argc, argv, - "AbcCdDeEFGhiI:klLmMNo:Op:PqrRsSt:uU:vVx:XYyZ", + "AbcCdDeEFGhiI:kK:lLmMNo:Op:PqrRsSt:uU:vVx:XYyZ", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -8595,6 +8741,12 @@ main(int argc, char **argv) usage(); } break; + case 'K': + dump_opt[c]++; + key_material = strdup(optarg); + /* redact key material in process table */ + while (*optarg != '\0') { *optarg++ = '*'; } + break; case 'o': error = set_global_var(optarg); if (error != 0) @@ -8689,7 +8841,7 @@ main(int argc, char **argv) verbose = MAX(verbose, 1); for (c = 0; c < 256; c++) { - if (dump_all && strchr("AeEFklLNOPrRSXy", c) == NULL) + if (dump_all && strchr("AeEFkKlLNOPrRSXy", c) == NULL) dump_opt[c] = 1; if (dump_opt[c]) dump_opt[c] += verbose; diff --git a/man/man8/zdb.8 b/man/man8/zdb.8 index a1447a1321b7..26c67dabd705 100644 --- a/man/man8/zdb.8 +++ b/man/man8/zdb.8 @@ -30,12 +30,14 @@ .Op Fl t Ar txg .Op Fl U Ar cache .Op Fl x Ar dumpdir +.Op Fl K Ar key .Op Ar poolname Ns Op / Ns Ar dataset Ns | Ns Ar objset-ID .Op Ar object Ns | Ns Ar range Ns … .Nm .Op Fl AdiPv .Op Fl e Oo Fl V Oc Oo Fl p Ar path Oc Ns … .Op Fl U Ar cache +.Op Fl K Ar key .Ar poolname Ns Op Ar / Ns Ar dataset Ns | Ns Ar objset-ID .Op Ar object Ns | Ns Ar range Ns … .Nm @@ -59,9 +61,11 @@ .Ar poolname Op Ar vdev Oo Ar metaslab Oc Ns … .Nm .Fl O +.Op Fl K Ar key .Ar dataset path .Nm .Fl r +.Op Fl K Ar key .Ar dataset path destination .Nm .Fl R @@ -418,6 +422,24 @@ The default value is 200. This option affects the performance of the .Fl c option. +.It Fl K , -key Ns = Ns Ar key +Decryption key needed to access an encrypted dataset. +This will cause +.Nm +to attempt to unlock the dataset using the encryption root, key format and other +encryption parameters on the given dataset. +.Nm +can still inspect pool and dataset structures on encrypted datasets without +unlocking them, but will not be able to access file names and attributes and +object contents. \fBWARNING:\fP The raw decryption key and any decrypted data +will be in user memory while +.Nm +is running. +Other user programs may be able to extract it by inspecting +.Nm +as it runs. +Exercise extreme caution when using this option in shared or uncontrolled +environments. .It Fl o , -option Ns = Ns Ar var Ns = Ns Ar value Ns … Set the given global libzpool variable to the provided value. The value must be an unsigned 32-bit integer. diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 421a8650bdbd..132df492541d 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -126,9 +126,9 @@ tags = ['functional', 'clean_mirror'] tests = ['zdb_002_pos', 'zdb_003_pos', 'zdb_004_pos', 'zdb_005_pos', 'zdb_006_pos', 'zdb_args_neg', 'zdb_args_pos', 'zdb_block_size_histogram', 'zdb_checksum', 'zdb_decompress', - 'zdb_display_block', 'zdb_label_checksum', 'zdb_object_range_neg', - 'zdb_object_range_pos', 'zdb_objset_id', 'zdb_decompress_zstd', - 'zdb_recover', 'zdb_recover_2'] + 'zdb_display_block', 'zdb_encrypted', 'zdb_label_checksum', + 'zdb_object_range_neg', 'zdb_object_range_pos', 'zdb_objset_id', + 'zdb_decompress_zstd', 'zdb_recover', 'zdb_recover_2'] pre = post = tags = ['functional', 'cli_root', 'zdb'] diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index 1a039742a617..efce46123c32 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -573,6 +573,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/cli_root/zdb/zdb_decompress.ksh \ functional/cli_root/zdb/zdb_decompress_zstd.ksh \ functional/cli_root/zdb/zdb_display_block.ksh \ + functional/cli_root/zdb/zdb_encrypted.ksh \ functional/cli_root/zdb/zdb_label_checksum.ksh \ functional/cli_root/zdb/zdb_object_range_neg.ksh \ functional/cli_root/zdb/zdb_object_range_pos.ksh \ diff --git a/tests/zfs-tests/tests/functional/cli_root/zdb/zdb_args_neg.ksh b/tests/zfs-tests/tests/functional/cli_root/zdb/zdb_args_neg.ksh index 5b34151e7588..168e7c18c3a3 100755 --- a/tests/zfs-tests/tests/functional/cli_root/zdb/zdb_args_neg.ksh +++ b/tests/zfs-tests/tests/functional/cli_root/zdb/zdb_args_neg.ksh @@ -57,7 +57,7 @@ set -A args "create" "add" "destroy" "import fakepool" \ "add raidz1 fakepool" "add raidz2 fakepool" \ "setvprop" "blah blah" "-%" "--?" "-*" "-=" \ "-a" "-f" "-g" "-j" "-n" "-o" "-p" "-p /tmp" \ - "-t" "-w" "-z" "-E" "-H" "-I" "-J" "-K" \ + "-t" "-w" "-z" "-E" "-H" "-I" "-J" \ "-Q" "-R" "-T" "-W" log_assert "Execute zdb using invalid parameters." diff --git a/tests/zfs-tests/tests/functional/cli_root/zdb/zdb_encrypted.ksh b/tests/zfs-tests/tests/functional/cli_root/zdb/zdb_encrypted.ksh new file mode 100755 index 000000000000..4572f64947a1 --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zdb/zdb_encrypted.ksh @@ -0,0 +1,69 @@ +#!/bin/ksh -p +# +# CDDL HEADER START +# +# This file and its contents are supplied under the terms of the +# Common Development and Distribution License ("CDDL"), version 1.0. +# You may only use this file in accordance with the terms of version +# 1.0 of the CDDL. +# +# A full copy of the text of the CDDL should have accompanied this +# source. A copy of the CDDL is also available via the Internet at +# http://www.illumos.org/license/CDDL. +# +# CDDL HEADER END +# + +# +# Copyright (c) 2017, Datto, Inc. All rights reserved. +# Copyright (c) 2023, Rob Norris +# + +. $STF_SUITE/include/libtest.shlib +. $STF_SUITE/tests/functional/cli_root/zfs_load-key/zfs_load-key_common.kshlib + +# +# DESCRIPTION: +# 'zdb -K ...' should enable reading from an encrypt dataset +# +# STRATEGY: +# 1. Create an encrypted dataset +# 2. Write some data to a file +# 3. Run zdb -dddd on the file, confirm it can't be read +# 4. Run zdb -K ... -ddddd on the file, confirm it can be read +# + +verify_runnable "both" + +dataset="$TESTPOOL/$TESTFS2" +file="$TESTDIR2/somefile" + +function cleanup +{ + datasetexists $dataset && destroy_dataset $dataset -f + default_cleanup_noexit +} + +log_onexit cleanup + +log_must default_setup_noexit $DISKS + +log_assert "'zdb -K' should enable reading from an encrypted dataset" + +log_must eval "echo $PASSPHRASE | zfs create -o mountpoint=$TESTDIR2" \ + "-o encryption=on -o keyformat=passphrase $dataset" + +echo 'my great encrypted text' > $file + +obj="$(ls -i $file | cut -d' ' -f1)" +size="$(wc -c < $file)" + +log_note "test file $file is objid $obj, size $size" + +sync_pool $TESTPOOL true + +log_must eval "zdb -dddd $dataset $obj | grep -q 'object encrypted'" + +log_must eval "zdb -K $PASSPHRASE -dddd $dataset $obj | grep -q 'size\s$size$'" + +log_pass "'zdb -K' enables reading from an encrypted dataset"