diff --git a/.build.yml b/.build.yml index 353e27a..966507e 100644 --- a/.build.yml +++ b/.build.yml @@ -8,6 +8,7 @@ packages: - pkg-config - libtss2-dev - libtspi-dev + - libssl-dev - mandoc - shellcheck - curl diff --git a/.gitignore b/.gitignore index 91e1d7a..6403a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ !src/** !man !man/** -!ext -!ext/** +!contrib +!contrib/** !initrd !initrd/** diff --git a/Makefile b/Makefile index 8069340..a3e1aa2 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ include configMakefile -LDDLLS := rt tspi $(OS_LD_LIBS) +LDDLLS := rt tspi crypto $(OS_LD_LIBS) PKGS := libzfs libzfs_core tss2-esys tss2-rc LDAR := $(LNCXXAR) $(foreach l,,-L$(BLDDIR)$(l)) $(foreach dll,$(LDDLLS),-l$(dll)) $(shell pkg-config --libs $(PKGS)) INCAR := $(foreach l,$(foreach l,,$(l)/include),-isystemext/$(l)) $(foreach l,,-isystem$(BLDDIR)$(l)/include) $(shell pkg-config --cflags $(PKGS)) @@ -99,10 +99,6 @@ $(OBJDIR)%$(OBJ) : $(SRCDIR)%.cpp @mkdir -p $(dir $@) $(CXX) $(CXXAR) $(INCAR) $(VERAR) $(DEF_TPH) -c -o$@ $^ -$(BLDDIR)test/%$(OBJ) : $(TSTDIR)%.cpp - @mkdir -p $(dir $@) - $(CXX) $(CXXAR) $(INCAR) -I$(SRCDIR) $(VERAR) -c -o$@ $^ - $(OUTDIR)dracut/usr/lib/dracut/modules.d/91tzpfms/% : $(INITRDDIR)dracut/% $(INITRD_HEADERS) @mkdir -p $(dir $@) $(AWK) -f pp.awk $< > $@ diff --git a/README.md b/README.md index 3e99b80..f103f33 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,16 @@ Essentially BitLocker, but for ZFS – a random raw key is generated and sealed to the TPM (both 2 and 1.x supported) with an additional optional password in front of it, tying the dataset to the platform and an additional optional secret (or to the posession of the back-up). +Additionally, 1.x TPMs support PCR binding with and without passwords. +2 TPMs support PCR binding without a password and PCR binding *OR* a password – both may be set, and any can be used to unseal (exclusive by default to prevent foot-guns). + Both dracut (with/without Plymouth) (with/without hostonly) (only on systemd systems, I don't have a test-bed for the non-systemd path) and initramfs-tools (with/without Plymouth) are supported for [ZFS-on-root](https://nabijaczleweli.xyz/content/blogn_t/005-low-curse-zfs-on-root.html) set-ups. ### Building -You'll need `pkg-config`, `shellcheck`, `libzfslinux-dev` (0.8.x and 2.[01].x work), `libtss2-dev`, `libtspi-dev`, and `make` should hopefully Just Work™ if you have a C++17-capable compiler. -The output binaries are trimmed of extraneous dependencies, so they're all just libc + libzfs and friends + the chosen TPM back-end, if any. +You'll need `pkg-config`, `shellcheck`, `libzfslinux-dev` (0.8.x and 2.[01].x work), `libtss2-dev`, `libtspi-dev`, `libssl-dev`, and `make` should hopefully Just Work™ if you have a C++17-capable compiler. +The output binaries are trimmed of extraneous dependencies, so they're all just libc + libzfs and friends + the chosen TPM back-end, if any + libcrypto for TPM2 PCR handling. `mandoc` is required for HTML manuals. Set `MANDOC=true` to forgo this. @@ -69,7 +72,7 @@ See the [repository README](//debian.nabijaczleweli.xyz/README) for more informa Build [`swtpm`](//github.com/stefanberger/swtpm), then prepare and run it: ```sh -swtpm_setup --tpmstate tpm2-state --tpm2 --createek --display --logfile /dev/stdout --overwrite +swtpm_setup --tpmstate tpm2-state --tpm2 --createek --display --logfile /dev/tty --overwrite swtpm socket --server type=tcp,port=2321 --ctrl type=tcp,port=2322 --tpm2 --tpmstate dir=tpm2-state --flags not-need-init --log level=10 ``` @@ -82,7 +85,7 @@ ln -s /usr/lib/i386-linux-gnu/libtss2-tcti-{swtpm,default}.so Build [`swtpm`](//github.com/stefanberger/swtpm), then prepare and run it and ([hopefully](//github.com/stefanberger/swtpm/issues/5#issuecomment-210607890)) [TrouSerS](//sourceforge.net/projects/trousers), as `root`/`tpm`: ```sh -swtpm_setup --tpmstate tpm1x-state --createek --display --logfile /dev/stdout --overwrite +swtpm_setup --tpmstate tpm1x-state --createek --display --logfile /dev/tty --overwrite swtpm cuse -n tpm --tpmstate dir=tpm1x-state --seccomp action=none --log level=10,file=/dev/fd/4 4>&1 swtpm_ioctl -i /dev/tpm TPM_DEVICE=/dev/tpm swtpm_bios diff --git a/configMakefile b/configMakefile index 51fc2f9..e5779d6 100644 --- a/configMakefile +++ b/configMakefile @@ -27,7 +27,7 @@ OS_LD_LIBS := CXXVER := $(shell $(CXX) --version) ifneq "$(findstring clang,$(CXXVER))" "" # GCC doesn't have this granularity - CXXSPECIFIC := -flto=full -pedantic -Wno-gnu-statement-expression -Wno-gnu-include-next -Wno-gnu-conditional-omitted-operand + CXXSPECIFIC := -flto=full -pedantic -Wno-gnu-statement-expression -Wno-gnu-include-next -Wno-gnu-conditional-omitted-operand -Wno-c++20-designator else CXXSPECIFIC := -flto endif @@ -62,6 +62,5 @@ OUTDIR := out/ BLDDIR := out/build/ OBJDIR := $(BLDDIR)obj/ SRCDIR := src/ -TSTDIR := test/ MANDIR := man/ INITRDDIR := initrd/ diff --git a/contrib/README b/contrib/README new file mode 100644 index 0000000..3128df2 --- /dev/null +++ b/contrib/README @@ -0,0 +1,2 @@ +These are development aids, not for distribution. +Link them to src/bin/ to build. diff --git a/contrib/zfs-tpm1x-muddle-pcrs.cpp b/contrib/zfs-tpm1x-muddle-pcrs.cpp new file mode 100644 index 0000000..c6f71cb --- /dev/null +++ b/contrib/zfs-tpm1x-muddle-pcrs.cpp @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: MIT */ + + +#include +#include + +#include "../main.hpp" +#include "../tpm1x.hpp" + + +#define THIS_BACKEND "TPM1.X" + + +int main(int argc, char ** argv) { + uint32_t * pcrs{}; + size_t pcrs_len{}; + bool just_read{}; + return do_main( + argc, argv, "RP:", "[-R] [-P PCR[,PCR]...]", + [&](auto o) { + switch(o) { + case 'R': + return just_read = true, 0; + case 'P': + return tpm1x_parse_pcrs(optarg, pcrs, pcrs_len); + default: + __builtin_unreachable(); + } + }, + [&](auto) { + return with_tpm1x_session([&](auto ctx, auto, auto) { + TSS_HTPM tpm_h{}; + TRY_TPM1X("extract TPM from context", Tspi_Context_GetTpmObject(ctx, &tpm_h)); + + + for(size_t i = 0; i < pcrs_len; i++) { + char buf[512]; + snprintf(buf, sizeof(buf), "muddle PCR %" PRIu32 "", pcrs[i]); + + BYTE * val{}; + uint32_t val_len{}; + + if(just_read) + TRY_TPM1X(buf, Tspi_TPM_PcrRead(tpm_h, pcrs[i], &val_len, &val)); + else { + BYTE data[TPM_SHA1_160_HASH_LEN]; + getrandom(data, sizeof(data), 0); + TRY_TPM1X(buf, Tspi_TPM_PcrExtend(tpm_h, pcrs[i], sizeof(data), data, nullptr, &val_len, &val)); + } + + + printf("PCR%u: ", pcrs[i]); + for(auto i = 0u; i < val_len; ++i) + printf("%02hhX", ((uint8_t *)val)[i]); + printf("\n"); + } + return 0; + }); + }); +} diff --git a/man/backend-tpm2.h b/man/backend-tpm2.h index f7c29cb..b2d8aa5 100644 --- a/man/backend-tpm2.h +++ b/man/backend-tpm2.h @@ -31,5 +31,6 @@ and the documentation at .Lk https:/\&/tpm2-tss.readthedocs.io . .Pp The TPM 2.0 specifications, mainly at -.Lk https:/\&/trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-1-Architecture-01.38.pdf +.Lk https:/\&/trustedcomputinggroup.org/resource/tpm-library-specification/ , +.Lk https:/\&/trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-1-Architecture-01.38.pdf , and related pages. diff --git a/man/zfs-tpm1x-change-key.8.pp b/man/zfs-tpm1x-change-key.8.pp index abfeaa7..db825a4 100644 --- a/man/zfs-tpm1x-change-key.8.pp +++ b/man/zfs-tpm1x-change-key.8.pp @@ -10,6 +10,7 @@ .Sh SYNOPSIS .Nm .Op Fl b Ar backup-file +.Op Fl P Ar PCR Ns Oo Ns Cm \&, Ns Ar PCR Oc Ns … .Ar dataset . .Sh DESCRIPTION @@ -62,7 +63,7 @@ tools .Li tzpfms.key is a colon-separated pair of hexadecimal-string (i.e. "4F7730" for "Ow0") blobs; the first one represents the RSA key protecting the blob, -and it is protected with either the password, if provided, or the SHA1 constant +and it is protected with either the passphrase, if provided, or the SHA1 constant .Li CE4CF677875B5EB8993591D5A9AF1ED24A3A8736 ; the second represents the sealed object containing the wrapping key, and is protected with the SHA1 constant @@ -79,13 +80,13 @@ or to issue a note for manual intervention into the standard error stream. A final verification should be made by running .Nm zfs-tpm1x-load-key Fl n Ar dataset . If that command succeeds, all is well, -but otherwise the dataset can be manually rolled back to a password with +but otherwise the dataset can be manually rolled back to a passphrase with .Nm zfs-tpm1x-clear-key Ar dataset .Pq or, if that fails to work, Nm zfs Cm change-key Fl o Li keyformat=passphrase Ar dataset , and you are hereby asked to report a bug, please. .Pp .Nm zfs-tpm1x-clear-key Ar dataset -can be used to clear the properties and go back to using a password. +can be used to clear the properties and go back to using a passphrase. . .Sh OPTIONS .Bl -tag -compact -width "-b backup-file" @@ -98,6 +99,15 @@ This back-up be stored securely, off-site. In case of a catastrophic event, the key can be loaded by running .Dl Nm zfs Cm load-key Ar dataset Li < Ar backup-file +.Pp +. +.It Fl P Ar PCR Ns Oo Ns Cm \&, Ns Ar PCR Oc Ns … +Bind the key to space- or comma-separated +.Ar PCR Ns s +\(em if they change, the wrapping key will not be able to be unsealed. +The minimum amount of PCRs for a PC TPM is +.Sy 24 Pq numbered Sy 0 Ns .. Ns Sy 23 . +For most, this is also the maximum. .El . #include "passphrase.h" @@ -105,3 +115,11 @@ In case of a catastrophic event, the key can be loaded by running #include "backend-tpm1x.h" . #include "common.h" +. +.Sh SEE ALSO +.\" Match this to zfs-tpm2-change-key.8: +PCR allocations: +.Lk https:/\&/wiki.archlinux.org/title/Trusted_Platform_Module#Accessing_PCR_registers +and +.Lk https:/\&/trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf , +Section 2.3.4 "PCR Usage", Table 1. diff --git a/man/zfs-tpm2-change-key.8.pp b/man/zfs-tpm2-change-key.8.pp index af3abd5..5ef66e2 100644 --- a/man/zfs-tpm2-change-key.8.pp +++ b/man/zfs-tpm2-change-key.8.pp @@ -10,6 +10,10 @@ .Sh SYNOPSIS .Nm .Op Fl b Ar backup-file +.Oo +.Fl P Ar algorithm Ns Cm \&: Ns Ar PCR Ns Oo Ns Cm \&, Ns Ar PCR Oc Ns … Ns Oo Cm + Ns Ar algorithm Ns Cm \&: Ns Ar PCR Ns Oo Ns Cm \&, Ns Ar PCR Oc Ns … Oc Ns … +.Op Fl A +.Oc .Ar dataset . .Sh DESCRIPTION @@ -49,7 +53,7 @@ The following properties are set on .It .Li xyz.nabijaczleweli:tzpfms.backend Ns = Ns Sy TPM2 .It -.Li xyz.nabijaczleweli:tzpfms.key Ns = Ns Ar ID of persistent object +.Li xyz.nabijaczleweli:tzpfms.key Ns = Ns Ar persistent-object-ID Ns Op Cm ;\& Ar algorithm Ns Cm \&: Ns Ar PCR Ns Oo Ns Cm \&, Ns Ar PCR Oc Ns … Ns Oo Cm + Ns Ar algorithm Ns Cm \&: Ns Ar PCR Ns Oo Ns Cm \&, Ns Ar PCR Oc Ns … Oc Ns … .El .Pp .Li tzpfms.backend @@ -60,10 +64,17 @@ tools .Pq namely Xr zfs-tpm2-change-key 8 , Xr zfs-tpm2-load-key 8 , and Xr zfs-tpm2-clear-key 8 . .Pp .Li tzpfms.key -is an integer representing the sealed object; +is an integer representing the sealed object, optionally followed by a semicolon and PCR list as specified with +.Fl P , +normalised to be +.Nm tpm-tools Ns -toolchain-compatible ; if needed, it can be passed to -.Nm tpm2_unseal Fl c Ev ${tzpfms.key} Op Fl p Ev ${password} -or equivalent for back-up +.Nm tpm2_unseal Fl c Ev ${tzpfms.key Ns Cm %% Ns Li ;* Ns Ev }\& +with +.Fl p Qq Li str:\& Ns Ev ${passphrase} +or +.Fl p Qq Li pcr:\& Ns Ev ${tzpfms.key Ns Cm # Ns Li *; Ns Ev }\& , +as the case may be, or equivalent, for back-up .Pq see Sx OPTIONS . If you have a sealed key you can access with that or equivalent tool and set both of these properties, it will funxion seamlessly. .Pp @@ -76,13 +87,13 @@ or to issue a note for manual intervention into the standard error stream. A final verification should be made by running .Nm zfs-tpm2-load-key Fl n Ar dataset . If that command succeeds, all is well, -but otherwise the dataset can be manually rolled back to a password with +but otherwise the dataset can be manually rolled back to a passphrase with .Nm zfs-tpm2-clear-key Ar dataset .Pq or, if that fails to work, Nm zfs Cm change-key Fl o Li keyformat=passphrase Ar dataset , and you are hereby asked to report a bug, please. .Pp .Nm zfs-tpm2-clear-key Ar dataset -can be used to free the TPM persistent object and go back to using a password. +can be used to free the TPM persistent object and go back to using a passphrase. . .Sh OPTIONS .Bl -tag -compact -width "-b backup-file" @@ -95,6 +106,48 @@ This back-up be stored securely, off-site. In case of a catastrophic event, the key can be loaded by running .Dl Nm zfs Cm load-key Ar dataset Li < Ar backup-file +.Pp +. +.It Fl P Ar algorithm Ns Cm \&: Ns Ar PCR Ns Oo Ns Cm \&, Ns Ar PCR Oc Ns … Ns Oo Cm + Ns Ar algorithm Ns Cm \&: Ns Ar PCR Ns Oo Ns Cm \&, Ns Ar PCR Oc Ns … Oc Ns … +Bind the key to space- or comma-separated +.Ar PCR Ns s +within their corresponding hashing +.Ar algorithm +\(em if they change, the wrapping key will not be able to be unsealed. +There are +.Sy 24 +PCRs, numbered +.Sy 0 Ns .. Ns Sy 23 . +.Pp +.Ar algorithm +may be any of case-insensitive +.Qq Sy sha1 , +.Qq Sy sha256 , +.Qq Sy sha384 , +.Qq Sy sha512 , +.Qq Sy sm3_256 , +.Qq Sy sm3-256 , +.Qq Sy sha3_256 , +.Qq Sy sha3-256 , +.Qq Sy sha3_384 , +.Qq Sy sha3-384 , +.Qq Sy sha3_512 , +or +.Qq Sy sha3-512 , +and must be supported by the TPM. +.Pp +. +.It Fl A +With +.Fl P , +also prompt for a passphrase. +This is skipped by default because the passphrase is +.Em OR Ns ed +with the PCR policy \(em the wrapping key can be unsealed +.Em either +passphraseless with the right PCRs +.Em or +with the passphrase, and this is usually not the intent. .El . #include "passphrase.h" @@ -105,3 +158,10 @@ In case of a catastrophic event, the key can be loaded by running . .Sh SEE ALSO .Xr tpm2_unseal 1 +.Pp +.\" Match this to zfs-tpm1x-change-key.8: +PCR allocations: +.Lk https:/\&/wiki.archlinux.org/title/Trusted_Platform_Module#Accessing_PCR_registers +and +.Lk https:/\&/trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf , +Section 2.3.4 "PCR Usage", Table 1. diff --git a/src/bin/zfs-tpm1x-change-key.cpp b/src/bin/zfs-tpm1x-change-key.cpp index f5391ff..f587206 100644 --- a/src/bin/zfs-tpm1x-change-key.cpp +++ b/src/bin/zfs-tpm1x-change-key.cpp @@ -5,6 +5,7 @@ // #include #define WRAPPING_KEY_LEN 32 +#include #include #include "../fd.hpp" @@ -23,8 +24,20 @@ int main(int argc, char ** argv) { const char * backup{}; + uint32_t * pcrs{}; + size_t pcrs_len{}; return do_main( - argc, argv, "b:", "[-b backup-file]", [&](auto) { backup = optarg; }, + argc, argv, "b:P:", "[-b backup-file] [-P PCR[,PCR]…]", + [&](auto o) { + switch(o) { + case 'b': + return backup = optarg, 0; + case 'P': + return tpm1x_parse_pcrs(optarg, pcrs, pcrs_len); + default: + __builtin_unreachable(); + } + }, [&](auto dataset) { REQUIRE_KEY_LOADED(dataset); @@ -37,6 +50,36 @@ int main(int argc, char ** argv) { TRY_TPM1X("extract TPM from context", Tspi_Context_GetTpmObject(ctx, &tpm_h)); + /// Do it early because it's a cmdline argument and to not ask for password if it fails + TSS_HOBJECT bound_pcrs{}; + quickscope_wrapper bound_pcrs_deleter{[&] { + if(bound_pcrs) + Tspi_Context_CloseObject(ctx, bound_pcrs); + }}; + if(pcrs_len) { + auto has_big = std::find_if(pcrs, pcrs + pcrs_len, [](auto p) { return p > 15; }) != pcrs + pcrs_len; + + TRY_TPM1X("create PCR list", + Tspi_Context_CreateObject(ctx, TSS_OBJECT_TYPE_PCRS, has_big ? TSS_PCRS_STRUCT_INFO_LONG : TSS_PCRS_STRUCT_DEFAULT, &bound_pcrs)); + + for(size_t i = 0; i < pcrs_len; i++) { + char buf[15 + 10 + 1]; // 4294967296 + snprintf(buf, sizeof(buf), "read PCR %" PRIu32 "", pcrs[i]); + + BYTE * val{}; + uint32_t val_len{}; + TRY_TPM1X(buf, Tspi_TPM_PcrRead(tpm_h, pcrs[i], &val_len, &val)); + quickscope_wrapper bound_pcrs_deleter{[&] { Tspi_Context_FreeMemory(ctx, val); }}; + + snprintf(buf, sizeof(buf), "save PCR %" PRIu32 " value", pcrs[i]); + TRY_TPM1X(buf, Tspi_PcrComposite_SetPcrValue(bound_pcrs, pcrs[i], val_len, val)); + } + + if(has_big) + TRY_TPM1X("set PCR locality", Tspi_PcrComposite_SetPcrLocality(bound_pcrs, TSS_LOCALITY_ALL)); + } + + uint8_t * wrap_key{}; TRY_TPM1X("get random data from TPM", Tspi_TPM_GetRandom(tpm_h, WRAPPING_KEY_LEN, &wrap_key)); if(backup) @@ -91,12 +134,8 @@ int main(int argc, char ** argv) { Tspi_Context_CloseObject(ctx, sealed_object); }}; - // This would need to replace the 0 below to handle PCRs - // TSS_HOBJECT bound_pcrs{}; // See tpm_sealdata.c from src:tpm-tools for more on flags here - // TRY_TPM1X("create PCR list", Tspi_Context_CreateObject(ctx, TSS_OBJECT_TYPE_PCRS, 0, &bound_pcrs)); - // quickscope_wrapper bound_pcrs_deleter{[&] { Tspi_Context_CloseObject(ctx, bound_pcrs); }}; - TRY_TPM1X("seal wrapping key data", Tspi_Data_Seal(sealed_object, parent_key, WRAPPING_KEY_LEN, wrap_key, 0)); + TRY_TPM1X("seal wrapping key data", Tspi_Data_Seal(sealed_object, parent_key, WRAPPING_KEY_LEN, wrap_key, bound_pcrs)); uint8_t * parent_key_blob{}; @@ -118,10 +157,10 @@ int main(int argc, char ** argv) { { auto cur = handle; for(auto i = 0u; i < parent_key_blob_len; ++i, cur += 2) - sprintf(cur, "%02X", parent_key_blob[i]); + sprintf(cur, "%02hhX", parent_key_blob[i]); *cur++ = ':'; for(auto i = 0u; i < sealed_object_blob_len; ++i, cur += 2) - sprintf(cur, "%02X", sealed_object_blob[i]); + sprintf(cur, "%02hhX", sealed_object_blob[i]); *cur++ = '\0'; } diff --git a/src/bin/zfs-tpm1x-load-key.cpp b/src/bin/zfs-tpm1x-load-key.cpp index 0b828c7..2047771 100644 --- a/src/bin/zfs-tpm1x-load-key.cpp +++ b/src/bin/zfs-tpm1x-load-key.cpp @@ -69,7 +69,7 @@ int main(int argc, char ** argv) { if(loaded_wrap_key_len != sizeof(wrap_key)) { fprintf(stderr, "Wrong sealed data length (%" PRIu32 " != %zu): ", loaded_wrap_key_len, sizeof(wrap_key)); for(auto i = 0u; i < loaded_wrap_key_len; ++i) - fprintf(stderr, "%02X", loaded_wrap_key[i]); + fprintf(stderr, "%02hhX", loaded_wrap_key[i]); fprintf(stderr, "\n"); return __LINE__; } diff --git a/src/bin/zfs-tpm2-change-key.cpp b/src/bin/zfs-tpm2-change-key.cpp index f80199d..0395709 100644 --- a/src/bin/zfs-tpm2-change-key.cpp +++ b/src/bin/zfs-tpm2-change-key.cpp @@ -20,13 +20,28 @@ int main(int argc, char ** argv) { const char * backup{}; + TPML_PCR_SELECTION pcrs{}; + bool allow_PCR_or_pass{}; return do_main( - argc, argv, "b:", "[-b backup-file]", [&](auto) { backup = optarg; }, + argc, argv, "b:P:A", "[-b backup-file] [-P algorithm:PCR[,PCR]…[+algorithm:PCR[,PCR]…]… [-A]]", + [&](auto o) { + switch(o) { + case 'b': + return backup = optarg, 0; + case 'P': + return tpm2_parse_pcrs(optarg, pcrs); + case 'A': + return allow_PCR_or_pass = true, 0; + default: + __builtin_unreachable(); + } + }, [&](auto dataset) { REQUIRE_KEY_LOADED(dataset); // https://software.intel.com/content/www/us/en/develop/articles/code-sample-protecting-secret-data-and-keys-using-intel-platform-trust-technology.html + // https://tpm2-software.github.io/2020/04/13/Disk-Encryption.html#pcr-policy-authentication---access-control-of-sealed-pass-phrase-on-tpm2-with-pcr-sealing // tssstartup // tpm2_createprimary -Q --hierarchy=o --key-context=prim.ctx // cat /tmp/sk | tpm2_create --hash-algorithm=sha256 --public=seal.pub --private=seal.priv --sealing-input=- --parent-context=prim.ctx @@ -36,14 +51,26 @@ int main(int argc, char ** argv) { // persistent-handle: 0x81000001 // // tpm2_unseal -Q --object-context=0x81000000 + // + // For PCRs: + // tpm2_startauthsession --session=session.ctx + // tpm2_policypcr -S session.ctx -l 'sha512:7+sha256:10' -L 5-10.policy3 + // tpm2_flushcontext session.ctx; rm session.ctx + // + tpm2_create{,primary} gain -l 'sha512:7+sha256:10', tpm2_create gains -L 5-10.policy3 + // + // tpm2_unseal -p pcr:'sha512:7+sha256:10' --object-context=0x81000000 + // or, longhand: + // tpm2_startauthsession --policy-session --session=session3.ctx + // tpm2_policypcr --session=session3.ctx --pcr-list='sha512:7+sha256:10' + // tpm2_unseal -p session:session3.ctx --object-context=0x81000000 + // tpm2_flushcontext session3.ctx; rm session3.ctx return with_tpm2_session([&](auto tpm2_ctx, auto tpm2_session) { TRY_MAIN(verify_backend(dataset, THIS_BACKEND, [&](auto previous_handle_s) { TPMI_DH_PERSISTENT previous_handle{}; - if(!parse_uint(previous_handle_s, previous_handle)) - fprintf(stderr, - "Couldn't parse previous persistent handle for dataset %s: %s. You might need to run \"tpm2_evictcontrol -c %s\" or equivalent!\n", - zfs_get_name(dataset), strerror(errno), previous_handle_s); + if(tpm2_parse_prop(zfs_get_name(dataset), previous_handle_s, previous_handle, nullptr)) + fprintf(stderr, "Couldn't parse previous persistent handle for dataset %s. You might need to run \"tpm2_evictcontrol -c %s\" or equivalent!\n", + zfs_get_name(dataset), previous_handle_s); else { if(tpm2_free_persistent(tpm2_ctx, tpm2_session, previous_handle)) fprintf(stderr, @@ -60,8 +87,8 @@ int main(int argc, char ** argv) { if(backup) TRY_MAIN(write_exact(backup, wrap_key, sizeof(wrap_key), 0400)); - TRY_MAIN(tpm2_seal(zfs_get_name(dataset), tpm2_ctx, tpm2_session, persistent_handle, tpm2_creation_metadata(zfs_get_name(dataset)), wrap_key, - sizeof(wrap_key))); + TRY_MAIN(tpm2_seal(zfs_get_name(dataset), tpm2_ctx, tpm2_session, persistent_handle, tpm2_creation_metadata(zfs_get_name(dataset)), pcrs, + allow_PCR_or_pass, wrap_key, sizeof(wrap_key))); bool ok = false; // Try to free the persistent handle if we're unsuccessful in actually using it later on quickscope_wrapper persistent_clearer{[&] { if(!ok && tpm2_free_persistent(tpm2_ctx, tpm2_session, persistent_handle)) @@ -72,12 +99,10 @@ int main(int argc, char ** argv) { }}; { - char persistent_handle_s[2 + sizeof(persistent_handle) * 2 + 1]; - if(auto written = snprintf(persistent_handle_s, sizeof(persistent_handle_s), "0x%" PRIX32, persistent_handle); - written < 0 || written >= static_cast(sizeof(persistent_handle_s))) { - return fprintf(stderr, "Truncated persistent_handle name? %d/%zu\n", written, sizeof(persistent_handle_s)), __LINE__; - } - TRY_MAIN(set_key_props(dataset, THIS_BACKEND, persistent_handle_s)); + char * prop{}; + TRY_MAIN(tpm2_unparse_prop(persistent_handle, pcrs, &prop)); + quickscope_wrapper prop_deleter{[&] { free(prop); }}; + TRY_MAIN(set_key_props(dataset, THIS_BACKEND, prop)); } TRY_MAIN(change_key(dataset, wrap_key)); @@ -85,5 +110,10 @@ int main(int argc, char ** argv) { ok = true; return 0; }); + }, + [&]() { + if(allow_PCR_or_pass && !pcrs.count) + return __LINE__; + return 0; }); } diff --git a/src/bin/zfs-tpm2-clear-key.cpp b/src/bin/zfs-tpm2-clear-key.cpp index 9d0cc46..691a1bf 100644 --- a/src/bin/zfs-tpm2-clear-key.cpp +++ b/src/bin/zfs-tpm2-clear-key.cpp @@ -12,6 +12,6 @@ int main(int argc, char ** argv) { TPMI_DH_PERSISTENT persistent_handle{}; return do_clear_main( argc, argv, THIS_BACKEND, - [&](auto dataset, auto persistent_handle_s) { return tpm2_parse_handle(zfs_get_name(dataset), persistent_handle_s, persistent_handle); }, + [&](auto dataset, auto persistent_handle_s) { return tpm2_parse_prop(zfs_get_name(dataset), persistent_handle_s, persistent_handle, nullptr); }, [&] { return with_tpm2_session([&](auto tpm2_ctx, auto tpm2_session) { return tpm2_free_persistent(tpm2_ctx, tpm2_session, persistent_handle); }); }); } diff --git a/src/bin/zfs-tpm2-load-key.cpp b/src/bin/zfs-tpm2-load-key.cpp index a5a664c..269d83c 100644 --- a/src/bin/zfs-tpm2-load-key.cpp +++ b/src/bin/zfs-tpm2-load-key.cpp @@ -25,12 +25,13 @@ int main(int argc, char ** argv) { TRY_MAIN(parse_key_props(dataset, THIS_BACKEND, handle_s)); TPMI_DH_PERSISTENT handle{}; - TRY_MAIN(tpm2_parse_handle(zfs_get_name(dataset), handle_s, handle)); + TPML_PCR_SELECTION pcrs{}; + TRY_MAIN(tpm2_parse_prop(zfs_get_name(dataset), handle_s, handle, &pcrs)); uint8_t wrap_key[WRAPPING_KEY_LEN]; TRY_MAIN(with_tpm2_session([&](auto tpm2_ctx, auto tpm2_session) { - TRY_MAIN(tpm2_unseal(zfs_get_name(dataset), tpm2_ctx, tpm2_session, handle, wrap_key, sizeof(wrap_key))); + TRY_MAIN(tpm2_unseal(zfs_get_name(dataset), tpm2_ctx, tpm2_session, handle, pcrs, wrap_key, sizeof(wrap_key))); return 0; })); diff --git a/src/main.hpp b/src/main.hpp index 9f34d2a..a74290d 100644 --- a/src/main.hpp +++ b/src/main.hpp @@ -19,8 +19,10 @@ } while(0) -template -int do_bare_main(int argc, char ** argv, const char * getoptions, const char * usage, const char * dataset_usage, G && getoptfn, M && main) { +template +static int do_bare_main( + int argc, char ** argv, const char * getoptions, const char * usage, const char * dataset_usage, G && getoptfn, M && main, + V && validate = []() { return 0; }) { const auto libz = TRY_PTR("initialise libzfs", libzfs_init()); quickscope_wrapper libz_deleter{[=] { libzfs_fini(libz); }}; @@ -39,47 +41,55 @@ int do_bare_main(int argc, char ** argv, const char * getoptions, const char * u fprintf(opt == 'h' ? stdout : stderr, "Usage: %s [-hV] %s%s%s\n", argv[0], usage, strlen(usage) ? " " : "", dataset_usage); return opt == 'h' ? 0 : __LINE__; case 'V': - printf("tzpfms version %s\n", TZPFMS_VERSION); + puts("tzpfms version " TZPFMS_VERSION); return 0; default: if constexpr(std::is_same_v, void>) getoptfn(opt); + else if constexpr(std::is_arithmetic_v>) + TRY_MAIN(getoptfn(opt)); else { if(auto err = getoptfn(opt)) return fprintf(stderr, "Usage: %s [-hV] %s%s%s\n", argv[0], usage, strlen(usage) ? " " : "", dataset_usage), err; } } + if(auto err = validate()) + return fprintf(stderr, "Usage: %s [-hV] %s%s%s\n", argv[0], usage, strlen(usage) ? " " : "", dataset_usage), err; return main(libz); } -template -int do_main(int argc, char ** argv, const char * getoptions, const char * usage, G && getoptfn, M && main) { - return do_bare_main(argc, argv, getoptions, usage, "", getoptfn, [&](auto libz) { - if(optind >= argc) - return fprintf(stderr, - "No dataset to act on?\n" - "Usage: %s [-hV] %s%s\n", - argv[0], usage, strlen(usage) ? " " : ""), - __LINE__; - auto dataset = TRY_PTR(nullptr, zfs_open(libz, argv[optind], ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME)); - quickscope_wrapper dataset_deleter{[&] { zfs_close(dataset); }}; +template +static int do_main( + int argc, char ** argv, const char * getoptions, const char * usage, G && getoptfn, M && main, V && validate = []() { return 0; }) { + return do_bare_main( + argc, argv, getoptions, usage, "dataset", getoptfn, + [&](auto libz) { + if(optind >= argc) + return fprintf(stderr, + "No dataset to act on?\n" + "Usage: %s [-hV] %s%sdataset\n", + argv[0], usage, strlen(usage) ? " " : ""), + __LINE__; + auto dataset = TRY_PTR(nullptr, zfs_open(libz, argv[optind], ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME)); + quickscope_wrapper dataset_deleter{[&] { zfs_close(dataset); }}; - { - char encryption_root[MAXNAMELEN]; - boolean_t dataset_is_root; - TRY("get encryption root", zfs_crypto_get_encryption_root(dataset, &dataset_is_root, encryption_root)); + { + char encryption_root[MAXNAMELEN]; + boolean_t dataset_is_root; + TRY("get encryption root", zfs_crypto_get_encryption_root(dataset, &dataset_is_root, encryption_root)); - if(!dataset_is_root && !strlen(encryption_root)) - return fprintf(stderr, "Dataset %s not encrypted?\n", zfs_get_name(dataset)), __LINE__; - else if(!dataset_is_root) { - fprintf(stderr, "Using dataset %s's encryption root %s instead.\n", zfs_get_name(dataset), encryption_root); - zfs_close(dataset); - dataset = TRY_PTR(nullptr, zfs_open(libz, encryption_root, ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME)); - } - } + if(!dataset_is_root && !strlen(encryption_root)) + return fprintf(stderr, "Dataset %s not encrypted?\n", zfs_get_name(dataset)), __LINE__; + else if(!dataset_is_root) { + fprintf(stderr, "Using dataset %s's encryption root %s instead.\n", zfs_get_name(dataset), encryption_root); + zfs_close(dataset); + dataset = TRY_PTR(nullptr, zfs_open(libz, encryption_root, ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME)); + } + } - return main(dataset); - }); + return main(dataset); + }, + validate); } diff --git a/src/tpm1x.cpp b/src/tpm1x.cpp index 07e30d2..cc09831 100644 --- a/src/tpm1x.cpp +++ b/src/tpm1x.cpp @@ -4,7 +4,9 @@ #include "tpm1x.hpp" #include "main.hpp" +#include "parse.hpp" +#include #include @@ -80,6 +82,39 @@ int tpm1x_prep_sealed_object(TSS_HCONTEXT ctx, TSS_HOBJECT & sealed_object, TSS_ return 0; } +/// https://trustedcomputinggroup.org/wp-content/uploads/TPM-Main-Part-1-Design-Principles_v1.2_rev116_01032011.pdf sections 4.4.7, .8 (L1228-1236): +/// > 7. A TPM implementation MUST provide 16 or more independent PCRs. These PCRs areidentified by index and MUST be numbered from 0 (that is, PCR0 through +/// > PCR15 are required for TCG compliance). Vendors MAY implement more registers for general-purpose use. Extra registers MUST be numbered contiguously +/// > from16 up to max – 1,where max is the maximum offered by the TPM +/// > 8. The TCG-protected capabilities that expose and modify the PCRs use a 32-bit index,indicating the maximum usable PCR index. However, TCG reserves +/// > register indices 230and higher for later versions of the specification. A TPM implementation MUST NOTprovide registers with indices greater than or +/// > equal to 230. +int tpm1x_parse_pcrs(char * arg, uint32_t *& pcrs, size_t & pcrs_len) { + size_t out_cap = 16; + pcrs = reinterpret_cast(TRY_PTR("allocate PCR list", calloc(out_cap, sizeof(uint32_t)))); + + char * sv{}; + for(arg = strtok_r(arg, ", ", &sv); arg; arg = strtok_r(nullptr, ", ", &sv)) { + uint32_t pcr; + if(!parse_uint(arg, pcr)) + return fprintf(stderr, "PCR %s: %s\n", arg, strerror(errno)), __LINE__; + if(pcr >= 230) + return fprintf(stderr, "PCR %s: too large (max 229).\n", arg), __LINE__; + + auto idx = std::upper_bound(pcrs, pcrs + pcrs_len, pcr) - pcrs; + if(!idx || pcrs[idx - 1] != pcr) { + if(pcrs_len >= out_cap) + pcrs = reinterpret_cast(TRY_PTR("allocate PCR list", reallocarray(pcrs, out_cap *= 2, sizeof(uint32_t)))); + + memmove(pcrs + idx + 1, pcrs + idx, (pcrs_len - idx) * sizeof(uint32_t)); + pcrs[idx] = pcr; + ++pcrs_len; + } + } + + return 0; +} + /// This feels suboptimal somehow, and yet static int fromxchar(uint8_t & out, char c) { diff --git a/src/tpm1x.hpp b/src/tpm1x.hpp index 4a20797..bfbd43c 100644 --- a/src/tpm1x.hpp +++ b/src/tpm1x.hpp @@ -102,3 +102,6 @@ extern int tpm1x_parse_handle(const char * dataset_name, char * handle_s, tpm1x_ /// Create sealed object, assign a policy and a known secret to it. extern int tpm1x_prep_sealed_object(TSS_HCONTEXT ctx, TSS_HOBJECT & sealed_object, TSS_HPOLICY & sealed_object_policy); + +/// Parse a comma- or space-separated number list. +extern int tpm1x_parse_pcrs(char * arg, uint32_t *& pcrs, size_t & pcrs_len); diff --git a/src/tpm2.cpp b/src/tpm2.cpp index be90522..4d422b3 100644 --- a/src/tpm2.cpp +++ b/src/tpm2.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include @@ -39,11 +41,12 @@ static int try_or_passphrase(const char * what, const char * what_for, ESYS_CONT TPM2B_DATA tpm2_creation_metadata(const char * dataset_name) { - TPM2B_DATA metadata{}; // 64 bytesish + TPM2B_DATA metadata{}; // 64 bytesish struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); - metadata.size = snprintf((char *)metadata.buffer, sizeof(metadata.buffer), "%" PRIu64 ".%09ld %s %s", ts.tv_sec, ts.tv_nsec, dataset_name, TZPFMS_VERSION) + 1; + metadata.size = + snprintf((char *)metadata.buffer, sizeof(metadata.buffer), "%" PRIu64 ".%09ld %s %s", ts.tv_sec, ts.tv_nsec, dataset_name, TZPFMS_VERSION) + 1; metadata.size = metadata.size > sizeof(metadata.buffer) ? sizeof(metadata.buffer) : metadata.size; // fprintf(stderr, "%" PRIu16 "/%zu: \"%s\"\n", metadata.size, sizeof(metadata.buffer), metadata.buffer); @@ -51,10 +54,159 @@ TPM2B_DATA tpm2_creation_metadata(const char * dataset_name) { } -int tpm2_parse_handle(const char * dataset_name, const char * handle_s, TPMI_DH_PERSISTENT & handle) { - if(!parse_uint(handle_s, handle)) +int tpm2_parse_prop(const char * dataset_name, char * handle_s, TPMI_DH_PERSISTENT & handle, TPML_PCR_SELECTION * pcrs) { + char * sv{}; + if(!parse_uint(handle_s = strtok_r(handle_s, ";", &sv), handle)) return fprintf(stderr, "Dataset %s's handle %s: %s.\n", dataset_name, handle_s, strerror(errno)), __LINE__; + if(auto p = strtok_r(nullptr, ";", &sv); p && pcrs) + TRY_MAIN(tpm2_parse_pcrs(p, *pcrs)); + + return 0; +} + + +/// Extension of the table used by tpm2-tools (tpm2_create et al.), which only has "s{m,ha}3_XXX", not "s{m,ha}3-XXX", and does case-sentitive comparisons +#define TPM2_HASH_ALGS_MAX_NAME_LEN 8 // sha3_512 +static const constexpr struct tpm2_hash_algs_t { + TPM2_ALG_ID alg; + const char * names[2]; +} tpm2_hash_algs[] = {{TPM2_ALG_SHA1, {"sha1"}}, + {TPM2_ALG_SHA256, {"sha256"}}, + {TPM2_ALG_SHA384, {"sha384"}}, + {TPM2_ALG_SHA512, {"sha512"}}, + {TPM2_ALG_SM3_256, {"sm3_256", "sm3-256"}}, + {TPM2_ALG_SHA3_256, {"sha3_256", "sha3-256"}}, + {TPM2_ALG_SHA3_384, {"sha3_384", "sha3-384"}}, + {TPM2_ALG_SHA3_512, {"sha3_512", "sha3-512"}}}; + +static constexpr bool is_tpm2_hash_algs_sorted() { + for(auto itr = std::begin(tpm2_hash_algs); itr != std::end(tpm2_hash_algs) - 1; ++itr) + if((itr + 1)->alg < itr->alg) + return false; + return true; +} +static_assert(is_tpm2_hash_algs_sorted()); // for the binary_search() below + +/// Assuming always != end: we always parse first +static const char * tpm2_hash_alg_name(TPM2_ALG_ID id) { + return std::lower_bound(std::begin(tpm2_hash_algs), std::end(tpm2_hash_algs), tpm2_hash_algs_t{id, {}}, + [&](auto && lhs, auto && rhs) { return lhs.alg < rhs.alg; }) + ->names[0]; +} + + +/// Nominally: +/// #define TPM2_MAX_PCRS 32 +/// #define TPM2_PCR_SELECT_MAX ((TPM2_MAX_PCRS + 7) / 8) +/// and +/// struct TPMS_PCR_SELECT { +/// UINT8 sizeofSelect; /* the size in octets of the pcrSelect array */ +/// BYTE pcrSelect[TPM2_PCR_SELECT_MAX]; /* the bit map of selected PCR */ +/// }; +/// +/// This works out to TPM2_PCR_SELECT_MAX=4, but most (all?) TPM2s have only 24 PCRs, meaning *any* request with sizeofSelect=sizeof(pcrSelect)=4 fails with +/// WARNING:esys:src/tss2-esys/api/Esys_CreatePrimary.c:393:Esys_CreatePrimary_Finish() Received TPM Error +/// ERROR:esys:src/tss2-esys/api/Esys_CreatePrimary.c:135:Esys_CreatePrimary() Esys Finish ErrorCode (0x000004c4) +/// Couldn't create primary encryption key: tpm:parameter(4):value is out of range or is not correct for the context +/// +/// Follow tpm2-tools and pretend TPM2_MAX_PCRS=24 => TPM2_PCR_SELECT_MAX=3 => sizeofSelect=3. +#define TPM2_MAX_PCRS_BUT_STRONGER 24 +#define TPM2_PCR_SELECT_MAX_BUT_STRONGER ((TPM2_MAX_PCRS_BUT_STRONGER + 7) / 8) +static_assert(TPM2_PCR_SELECT_MAX_BUT_STRONGER <= sizeof(TPMS_PCR_SELECT::pcrSelect)); + +int tpm2_parse_pcrs(char * arg, TPML_PCR_SELECTION & pcrs) { + TPMS_PCR_SELECTION * bank = pcrs.pcrSelections; + + char * ph_sv{}; + for(auto per_hash = strtok_r(arg, "+", &ph_sv); per_hash; per_hash = strtok_r(nullptr, "+", &ph_sv), ++bank) { + while(*per_hash == ' ') + ++per_hash; + + if(bank == pcrs.pcrSelections + (sizeof(pcrs.pcrSelections) / sizeof(*pcrs.pcrSelections))) // == TPM2_NUM_PCR_BANKS + return fprintf(stderr, "Too many PCR banks specified! Can only have up to %zu\n", sizeof(pcrs.pcrSelections) / sizeof(*pcrs.pcrSelections)), __LINE__; + + if(auto sep = strchr(per_hash, ':')) { + *sep = '\0'; + auto values = sep + 1; + + if(auto alg = std::find_if( + std::begin(tpm2_hash_algs), std::end(tpm2_hash_algs), + [&](auto && alg) { return std::any_of(std::begin(alg.names), std::end(alg.names), [&](auto && nm) { return nm && !strcasecmp(per_hash, nm); }); }); + alg != std::end(tpm2_hash_algs)) + bank->hash = alg->alg; + else { + if(!parse_uint(per_hash, bank->hash) || !std::binary_search(std::begin(tpm2_hash_algs), std::end(tpm2_hash_algs), tpm2_hash_algs_t{bank->hash, {}}, + [&](auto && lhs, auto && rhs) { return lhs.alg < rhs.alg; })) { + fprintf(stderr, + "Unknown hash algorithm %s.\n" + "Can be any of case-insensitive ", + per_hash); + auto first = true; + for(auto && alg : tpm2_hash_algs) + for(auto && nm : alg.names) + if(nm) + fprintf(stderr, "%s%s", first ? "" : ", ", nm), first = false; + return fputs(".\n", stderr), __LINE__; + } + } + + bank->sizeofSelect = TPM2_PCR_SELECT_MAX_BUT_STRONGER; + if(!strcasecmp(values, "all")) + memset(bank->pcrSelect, 0xFF, bank->sizeofSelect); + else if(!strcasecmp(values, "none")) + ; // already 0 + else { + char * sv{}; + for(values = strtok_r(values, ", ", &sv); values; values = strtok_r(nullptr, ", ", &sv)) { + uint8_t pcr; + if(!parse_uint(values, pcr)) + return fprintf(stderr, "PCR %s: %s\n", values, strerror(errno)), __LINE__; + if(pcr > TPM2_MAX_PCRS_BUT_STRONGER - 1) + return fprintf(stderr, "PCR %s: %s, max %u\n", values, strerror(ERANGE), TPM2_MAX_PCRS_BUT_STRONGER - 1), __LINE__; + + bank->pcrSelect[pcr / 8] |= 1 << (pcr % 8); + } + } + } else + return fprintf(stderr, "PCR bank \"%s\": no algorithm; need alg:PCR[,PCR]...\n", per_hash), __LINE__; + } + + pcrs.count = bank - pcrs.pcrSelections; + return 0; +} + +int tpm2_unparse_prop(TPMI_DH_PERSISTENT persistent_handle, const TPML_PCR_SELECTION & pcrs, char ** prop) { + // 0xFFFFFFFF;sha3_512:00,01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17,18,19,20,21,22+sha3_... + *prop = TRY_PTR("allocate property value", + reinterpret_cast(malloc(2 + 8 + pcrs.count * (1 + TPM2_HASH_ALGS_MAX_NAME_LEN + (TPM2_MAX_PCRS_BUT_STRONGER - 1) * 3) + 1))); + + auto cur = *prop; + cur += sprintf(cur, "0x%" PRIX32 "", persistent_handle); + + auto pre = ';'; + for(size_t i = 0; i < pcrs.count; ++i) { + auto && sel = pcrs.pcrSelections[i]; + *cur++ = std::exchange(pre, '+'); + + auto nm = tpm2_hash_alg_name(sel.hash); + auto nm_len = strlen(nm); + memcpy(cur, nm, nm_len), cur += nm_len; + + if(std::all_of(sel.pcrSelect, sel.pcrSelect + sel.sizeofSelect, [](auto b) { return b == 0x00; })) + memcpy(cur, ":none", strlen(":none")), cur += strlen(":none"); + else if(std::all_of(sel.pcrSelect, sel.pcrSelect + sel.sizeofSelect, [](auto b) { return b == 0xFF; })) + memcpy(cur, ":all", strlen(":all")), cur += strlen(":all"); + else { + bool first = true; + for(size_t j = 0; j < sel.sizeofSelect; ++j) + for(uint8_t b = 0; b < 8; ++b) + if(sel.pcrSelect[j] & (1 << b)) + cur += sprintf(cur, "%c%zu", std::exchange(first, false) ? ':' : ',', j * 8 + b); + } + } + + *cur = '\0'; return 0; } @@ -62,7 +214,7 @@ int tpm2_parse_handle(const char * dataset_name, const char * handle_s, TPMI_DH_ int tpm2_generate_rand(ESYS_CONTEXT * tpm2_ctx, void * into, size_t length) { TPM2B_DIGEST * rand{}; TRY_TPM2("get random data from TPM", Esys_GetRandom(tpm2_ctx, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, length, &rand)); - quickscope_wrapper rand_deleter{[=] { Esys_Free(rand); }}; + quickscope_wrapper rand_deleter{[&] { Esys_Free(rand); }}; if(rand->size != length) return fprintf(stderr, "Wrong random size: wanted %zu, got %" PRIu16 " bytes.\n", length, rand->size), __LINE__; @@ -73,10 +225,10 @@ int tpm2_generate_rand(ESYS_CONTEXT * tpm2_ctx, void * into, size_t length) { static int tpm2_find_unused_persistent_non_platform(ESYS_CONTEXT * tpm2_ctx, TPMI_DH_PERSISTENT & persistent_handle) { - TPMS_CAPABILITY_DATA * cap; // TODO: check for more data? + TPMS_CAPABILITY_DATA * cap{}; // TODO: check for more data? TRY_TPM2("Read used persistent TPM handles", Esys_GetCapability(tpm2_ctx, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, TPM2_CAP_HANDLES, TPM2_PERSISTENT_FIRST, TPM2_MAX_CAP_HANDLES, nullptr, &cap)); - quickscope_wrapper cap_deleter{[=] { Esys_Free(cap); }}; + quickscope_wrapper cap_deleter{[&] { Esys_Free(cap); }}; persistent_handle = 0; switch(cap->data.handles.count) { @@ -98,8 +250,69 @@ static int tpm2_find_unused_persistent_non_platform(ESYS_CONTEXT * tpm2_ctx, TPM return 0; } +template +static int tpm2_police_pcrs(ESYS_CONTEXT * tpm2_ctx, const TPML_PCR_SELECTION & pcrs, TPM2_SE session_type, F && with_session) { + if(!pcrs.count) + return with_session(ESYS_TR_NONE); + + TPM2B_DIGEST digested_pcrs{}; + digested_pcrs.size = SHA256_DIGEST_LENGTH; + static_assert(sizeof(TPM2B_DIGEST::buffer) >= SHA256_DIGEST_LENGTH); + + { + SHA256_CTX ctx; + new_pcrs: + std::optional update_count; + SHA256_Init(&ctx); + auto pcrs_left = pcrs; + while(std::any_of(pcrs_left.pcrSelections, pcrs_left.pcrSelections + pcrs_left.count, + [](auto && sel) { return std::any_of(sel.pcrSelect, sel.pcrSelect + sel.sizeofSelect, [](auto b) { return b; }); })) { + uint32_t out_upcnt{}; + TPML_PCR_SELECTION * out_sel{}; + TPML_DIGEST * out_val{}; + TRY_TPM2("read PCRs", Esys_PCR_Read(tpm2_ctx, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &pcrs_left, &out_upcnt, &out_sel, &out_val)); + quickscope_wrapper out_deleter{[&] { Esys_Free(out_val), Esys_Free(out_sel); }}; + + if(update_count && update_count != out_upcnt) + goto new_pcrs; + update_count = out_upcnt; + + if(!out_val->count) { // this can happen with SHA1 disabled, for example + auto first = true; + fputs("No PCRs when asking for ", stderr); + for(size_t i = 0; i < pcrs_left.count; ++i) + if(std::any_of(pcrs_left.pcrSelections[i].pcrSelect, pcrs_left.pcrSelections[i].pcrSelect + pcrs_left.pcrSelections[i].sizeofSelect, + [](auto b) { return b; })) + fprintf(stderr, "%s%s", std::exchange(first, false) ? "" : ", ", tpm2_hash_alg_name(pcrs_left.pcrSelections[i].hash)); + return fputs(": does the TPM support the algorithm?\n", stderr), __LINE__; + } + + for(size_t i = 0; i < out_val->count; ++i) + SHA256_Update(&ctx, out_val->digests[i].buffer, out_val->digests[i].size); + + for(size_t i = 0; i < out_sel->count; ++i) + for(size_t j = 0u; j < out_sel->pcrSelections[i].sizeofSelect; ++j) + pcrs_left.pcrSelections[i].pcrSelect[j] &= ~out_sel->pcrSelections[i].pcrSelect[j]; + } + + SHA256_Final(digested_pcrs.buffer, &ctx); + } + + + ESYS_TR pcr_session = ESYS_TR_NONE; + quickscope_wrapper tpm2_session_deleter{[&] { Esys_FlushContext(tpm2_ctx, pcr_session); }}; + + TRY_TPM2("start PCR session", Esys_StartAuthSession(tpm2_ctx, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, nullptr, session_type, + &tpm2_session_key, TPM2_ALG_SHA256, &pcr_session)); + + + TRY_TPM2("create PCR policy", Esys_PolicyPCR(tpm2_ctx, pcr_session, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &digested_pcrs, &pcrs)); + + return with_session(pcr_session); +} + int tpm2_seal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_session, TPMI_DH_PERSISTENT & persistent_handle, const TPM2B_DATA & metadata, - void * data, size_t data_len) { + const TPML_PCR_SELECTION & pcrs, bool allow_PCR_or_pass, void * data, size_t data_len) { ESYS_TR primary_handle = ESYS_TR_NONE; quickscope_wrapper primary_handle_deleter{[&] { Esys_FlushContext(tpm2_ctx, primary_handle); }}; @@ -118,21 +331,10 @@ int tpm2_seal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_sessio pub.publicArea.parameters.rsaDetail.scheme.scheme = TPM2_ALG_NULL; pub.publicArea.parameters.rsaDetail.keyBits = 2048; pub.publicArea.parameters.rsaDetail.exponent = 0; - - const TPML_PCR_SELECTION pcrs{}; - - TPM2B_PUBLIC * public_ret{}; - TPM2B_CREATION_DATA * creation_data{}; - TPM2B_DIGEST * creation_hash{}; - TPMT_TK_CREATION * creation_ticket{}; TRY_MAIN(try_or_passphrase("create primary encryption key", "TPM2 owner hierarchy", tpm2_ctx, TPM2_RC_BAD_AUTH, ESYS_TR_RH_OWNER, [&] { return Esys_CreatePrimary(tpm2_ctx, ESYS_TR_RH_OWNER, tpm2_session, ESYS_TR_NONE, ESYS_TR_NONE, &primary_sens, &pub, &metadata, &pcrs, &primary_handle, - &public_ret, &creation_data, &creation_hash, &creation_ticket); + nullptr, nullptr, nullptr, nullptr); })); - quickscope_wrapper creation_ticket_deleter{[=] { Esys_Free(creation_ticket); }}; - quickscope_wrapper creation_hash_deleter{[=] { Esys_Free(creation_hash); }}; - quickscope_wrapper creation_data_deleter{[=] { Esys_Free(creation_data); }}; - quickscope_wrapper public_ret_deleter{[=] { Esys_Free(public_ret); }}; // TSS2_RC Esys_CertifyCreation ( ESYS_CONTEXT * esysContext, // ESYS_TR signHandle, @@ -149,11 +351,19 @@ int tpm2_seal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_sessio // ) } + TPM2B_DIGEST policy_digest{}; + if(pcrs.count) + TRY_MAIN(tpm2_police_pcrs(tpm2_ctx, pcrs, TPM2_SE_TRIAL, [&](auto pcr_session) { + TPM2B_DIGEST * dgst{}; + TRY_TPM2("get PCR policy digest", Esys_PolicyGetDigest(tpm2_ctx, pcr_session, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &dgst)); + quickscope_wrapper dgst_deleter{[&] { Esys_Free(dgst); }}; + policy_digest = *dgst; + return 0; + })); TPM2B_PRIVATE * sealant_private{}; TPM2B_PUBLIC * sealant_public{}; - quickscope_wrapper sealant_public_deleter{[=] { Esys_Free(sealant_public); }}; - quickscope_wrapper sealant_private_deleter{[=] { Esys_Free(sealant_private); }}; + quickscope_wrapper sealant_deleter{[&] { Esys_Free(sealant_public), Esys_Free(sealant_private); }}; /// This is the object with the actual sealed data in it { @@ -161,39 +371,34 @@ int tpm2_seal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_sessio secret_sens.sensitive.data.size = data_len; memcpy(secret_sens.sensitive.data.buffer, data, secret_sens.sensitive.data.size); - { + if(!pcrs.count || allow_PCR_or_pass) { char what_for[ZFS_MAX_DATASET_NAME_LEN + 38 + 1]; snprintf(what_for, sizeof(what_for), "%s TPM2 wrapping key (or empty for none)", dataset); uint8_t * passphrase{}; size_t passphrase_len{}; - TRY_MAIN(read_new_passphrase("%s TPM2 wrapping key (or empty for none)", passphrase, passphrase_len, - sizeof(TPM2B_SENSITIVE_CREATE::sensitive.userAuth.buffer))); + TRY_MAIN(read_new_passphrase(what_for, passphrase, passphrase_len, sizeof(TPM2B_SENSITIVE_CREATE::sensitive.userAuth.buffer))); quickscope_wrapper passphrase_deleter{[&] { free(passphrase); }}; secret_sens.sensitive.userAuth.size = passphrase_len; memcpy(secret_sens.sensitive.userAuth.buffer, passphrase, secret_sens.sensitive.userAuth.size); } + // Same args as tpm2-tools' tpm2_create(1) TPM2B_PUBLIC pub{}; - pub.publicArea.type = TPM2_ALG_KEYEDHASH; - pub.publicArea.nameAlg = TPM2_ALG_SHA256; - pub.publicArea.objectAttributes = TPMA_OBJECT_FIXEDTPM | TPMA_OBJECT_FIXEDPARENT | TPMA_OBJECT_USERWITHAUTH; + pub.publicArea.type = TPM2_ALG_KEYEDHASH; + pub.publicArea.nameAlg = TPM2_ALG_SHA256; + pub.publicArea.objectAttributes = + TPMA_OBJECT_FIXEDTPM | TPMA_OBJECT_FIXEDPARENT | ((pcrs.count && !secret_sens.sensitive.userAuth.size) ? 0 : TPMA_OBJECT_USERWITHAUTH); pub.publicArea.parameters.keyedHashDetail.scheme.scheme = TPM2_ALG_NULL; + pub.publicArea.authPolicy = policy_digest; - const TPML_PCR_SELECTION pcrs{}; - - TPM2B_CREATION_DATA * creation_data{}; - TPM2B_DIGEST * creation_hash{}; - TPMT_TK_CREATION * creation_ticket{}; TRY_TPM2("create key seal", Esys_Create(tpm2_ctx, primary_handle, tpm2_session, ESYS_TR_NONE, ESYS_TR_NONE, &secret_sens, &pub, &metadata, &pcrs, - &sealant_private, &sealant_public, &creation_data, &creation_hash, &creation_ticket)); - quickscope_wrapper creation_ticket_deleter{[=] { Esys_Free(creation_ticket); }}; - quickscope_wrapper creation_hash_deleter{[=] { Esys_Free(creation_hash); }}; - quickscope_wrapper creation_data_deleter{[=] { Esys_Free(creation_data); }}; + &sealant_private, &sealant_public, nullptr, nullptr, nullptr)); } + ESYS_TR sealed_handle = ESYS_TR_NONE; quickscope_wrapper sealed_handle_deleter{[&] { Esys_FlushContext(tpm2_ctx, sealed_handle); }}; @@ -214,7 +419,9 @@ int tpm2_seal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_sessio return 0; } -int tpm2_unseal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_session, TPMI_DH_PERSISTENT persistent_handle, void * data, size_t data_len) { +int tpm2_unseal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_session, TPMI_DH_PERSISTENT persistent_handle, const TPML_PCR_SELECTION & pcrs, + void * data, size_t data_len) { + // Esys_FlushContext(tpm2_ctx, tpm2_session); char what_for[ZFS_MAX_DATASET_NAME_LEN + 18 + 1]; snprintf(what_for, sizeof(what_for), "%s TPM2 wrapping key", dataset); @@ -222,10 +429,22 @@ int tpm2_unseal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_sess ESYS_TR pandle; TRY_TPM2("convert persistent handle to object", Esys_TR_FromTPMPublic(tpm2_ctx, persistent_handle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &pandle)); + TPM2B_SENSITIVE_DATA * unsealed{}; - quickscope_wrapper unsealed_deleter{[=] { Esys_Free(unsealed); }}; - TRY_MAIN(try_or_passphrase("unseal wrapping key", what_for, tpm2_ctx, TPM2_RC_AUTH_FAIL, pandle, - [&] { return Esys_Unseal(tpm2_ctx, pandle, tpm2_session, ESYS_TR_NONE, ESYS_TR_NONE, &unsealed); })); + quickscope_wrapper unsealed_deleter{[&] { Esys_Free(unsealed); }}; + auto unseal = [&](auto sess) { return Esys_Unseal(tpm2_ctx, pandle, sess, ESYS_TR_NONE, ESYS_TR_NONE, &unsealed); }; + TRY_MAIN(tpm2_police_pcrs(tpm2_ctx, pcrs, TPM2_SE_POLICY, [&](auto pcr_session) { + // In case there's (PCR policy || passphrase): try PCR once; if it fails, fall back to passphrase + if(pcr_session != ESYS_TR_NONE) { + if(auto err = unseal(pcr_session); err != TPM2_RC_SUCCESS) + fprintf(stderr, "Couldn't %s with PCR policy: %s\n", "unseal wrapping key", Tss2_RC_Decode(err)); + else + return 0; + } + + return try_or_passphrase("unseal wrapping key", what_for, tpm2_ctx, TPM2_RC_AUTH_FAIL, pandle, [&] { return unseal(tpm2_session); }); + })); + if(unsealed->size != data_len) return fprintf(stderr, "Unsealed data has wrong length %" PRIu16 ", expected %zu!\n", unsealed->size, data_len), __LINE__; diff --git a/src/tpm2.hpp b/src/tpm2.hpp index 07377bb..db7d8dd 100644 --- a/src/tpm2.hpp +++ b/src/tpm2.hpp @@ -14,6 +14,10 @@ #define TRY_TPM2(what, ...) TRY_GENERIC(what, , != TPM2_RC_SUCCESS, _try_ret, __LINE__, Tss2_RC_Decode, __VA_ARGS__) +// https://github.com/tpm2-software/tpm2-tss/blob/49146d926ccb0fd3c3ee064455eb02356e0cdf90/test/integration/esys-create-session-auth.int.c#L218 +static const constexpr TPMT_SYM_DEF tpm2_session_key{.algorithm = TPM2_ALG_AES, .keyBits = {.aes = 128}, .mode = {.aes = TPM2_ALG_CFB}}; + + template int with_tpm2_session(F && func) { // https://trustedcomputinggroup.org/wp-content/uploads/TSS_ESAPI_v1p00_r05_pubrev.pdf @@ -29,15 +33,8 @@ int with_tpm2_session(F && func) { ESYS_TR tpm2_session = ESYS_TR_NONE; quickscope_wrapper tpm2_session_deleter{[&] { Esys_FlushContext(tpm2_ctx, tpm2_session); }}; - { - // https://github.com/tpm2-software/tpm2-tss/blob/master/test/integration/esys-create-session-auth.int.c#L218 - TPMT_SYM_DEF session_key{}; - session_key.algorithm = TPM2_ALG_AES; - session_key.keyBits.aes = 128; - session_key.mode.aes = TPM2_ALG_CFB; - TRY_TPM2("authenticate with TPM", Esys_StartAuthSession(tpm2_ctx, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, nullptr, - TPM2_SE_HMAC, &session_key, TPM2_ALG_SHA256, &tpm2_session)); - } + TRY_TPM2("authenticate with TPM", Esys_StartAuthSession(tpm2_ctx, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, nullptr, TPM2_SE_HMAC, + &tpm2_session_key, TPM2_ALG_SHA256, &tpm2_session)); return func(tpm2_ctx, tpm2_session); } @@ -45,10 +42,15 @@ int with_tpm2_session(F && func) { extern TPM2B_DATA tpm2_creation_metadata(const char * dataset_name); /// Parse a persistent handle name as stored in a ZFS property -extern int tpm2_parse_handle(const char * dataset_name, const char * handle_s, TPMI_DH_PERSISTENT & handle); +extern int tpm2_parse_prop(const char * dataset_name, char * handle_s, TPMI_DH_PERSISTENT & handle, TPML_PCR_SELECTION * pcrs); +extern int tpm2_unparse_prop(TPMI_DH_PERSISTENT persistent_handle, const TPML_PCR_SELECTION & pcrs, char ** prop); + +/// `alg:PCR[,PCR]...[+alg:PCR[,PCR]...]...`; all separators can have spaces +extern int tpm2_parse_pcrs(char * arg, TPML_PCR_SELECTION & pcrs); extern int tpm2_generate_rand(ESYS_CONTEXT * tpm2_ctx, void * into, size_t length); extern int tpm2_seal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_session, TPMI_DH_PERSISTENT & persistent_handle, const TPM2B_DATA & metadata, - void * data, size_t data_len); -extern int tpm2_unseal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_session, TPMI_DH_PERSISTENT persistent_handle, void * data, size_t data_len); + const TPML_PCR_SELECTION & pcrs, bool allow_PCR_or_pass, void * data, size_t data_len); +extern int tpm2_unseal(const char * dataset, ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_session, TPMI_DH_PERSISTENT persistent_handle, + const TPML_PCR_SELECTION & pcrs, void * data, size_t data_len); extern int tpm2_free_persistent(ESYS_CONTEXT * tpm2_ctx, ESYS_TR tpm2_session, TPMI_DH_PERSISTENT persistent_handle); diff --git a/tzpfms.sublime-project b/tzpfms.sublime-project index 59ee7a8..8aa8932 100644 --- a/tzpfms.sublime-project +++ b/tzpfms.sublime-project @@ -25,6 +25,11 @@ "name": "Source", "path": "src" }, + { + "follow_symlinks": true, + "name": "Misc source", + "path": "contrib" + }, { "follow_symlinks": true, "name": "Initrd plug-ins",