Add PCR binding. password => passphrase in manuals

All logically distinct modes are now:
  TPM1.X: passphraseless, PCRs, passphrase, passphrase & PCRs
  TPM2:   passphraseless, PCRs, passphrase, passphrase | PCRs

TPM2 sees a backward-incompatible ";pcr list" addition to its handle

Cf. https://twitter.com/nabijaczleweli/status/1463707170793562117
This commit is contained in:
наб 2021-11-28 01:33:58 +01:00
parent de9b591546
commit 49f0a05c33
No known key found for this signature in database
GPG Key ID: BCFD0B018D2658F1
21 changed files with 613 additions and 129 deletions

View File

@ -8,6 +8,7 @@ packages:
- pkg-config
- libtss2-dev
- libtspi-dev
- libssl-dev
- mandoc
- shellcheck
- curl

4
.gitignore vendored
View File

@ -14,7 +14,7 @@
!src/**
!man
!man/**
!ext
!ext/**
!contrib
!contrib/**
!initrd
!initrd/**

View File

@ -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 $< > $@

View File

@ -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

View File

@ -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/

2
contrib/README Normal file
View File

@ -0,0 +1,2 @@
These are development aids, not for distribution.
Link them to src/bin/ to build.

View File

@ -0,0 +1,60 @@
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <sys/random.h>
#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;
});
});
}

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -5,6 +5,7 @@
// #include <sys/zio_crypt.h>
#define WRAPPING_KEY_LEN 32
#include <algorithm>
#include <stdio.h>
#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';
}

View File

@ -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__;
}

View File

@ -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<int>(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;
});
}

View File

@ -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); }); });
}

View File

@ -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;
}));

View File

@ -19,8 +19,10 @@
} while(0)
template <class G, class M>
int do_bare_main(int argc, char ** argv, const char * getoptions, const char * usage, const char * dataset_usage, G && getoptfn, M && main) {
template <class G, class M, class V = int (*)()>
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<std::invoke_result_t<G, decltype(opt)>, void>)
getoptfn(opt);
else if constexpr(std::is_arithmetic_v<std::invoke_result_t<G, decltype(opt)>>)
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 <class G, class M>
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, "<dataset>", getoptfn, [&](auto libz) {
if(optind >= argc)
return fprintf(stderr,
"No dataset to act on?\n"
"Usage: %s [-hV] %s%s<dataset>\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 <class G, class M, class V = int (*)()>
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);
}

View File

@ -4,7 +4,9 @@
#include "tpm1x.hpp"
#include "main.hpp"
#include "parse.hpp"
#include <algorithm>
#include <stdlib.h>
@ -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<uint32_t *>(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<uint32_t *>(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) {

View File

@ -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);

View File

@ -8,6 +8,8 @@
#include <algorithm>
#include <inttypes.h>
#include <openssl/sha.h>
#include <optional>
#include <time.h>
@ -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<char *>(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 <class F>
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<uint32_t> 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__;

View File

@ -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 <class F>
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);

View File

@ -25,6 +25,11 @@
"name": "Source",
"path": "src"
},
{
"follow_symlinks": true,
"name": "Misc source",
"path": "contrib"
},
{
"follow_symlinks": true,
"name": "Initrd plug-ins",