[Skiboot] [PATCH v2 2/6] secvar/secboot_tpm: unify behavior for bank hash check and secboot header check
Eric Richter
erichte at linux.ibm.com
Wed Nov 3 02:04:43 AEDT 2021
On 11/2/21 9:16 AM, Daniel Axtens wrote:
> Eric Richter <erichte at linux.ibm.com> writes:
>
>> As the PNOR variable space cannot be locked, the data must be integrity
>> checked when loaded to ensure it has not beeen modified by an unauthorized
>> party. In the event that a modification has been detected (i.e. hash mismatch),
>> we must not load in data that could potentially be compromised.
>>
>> However, the previous code was a bit overzealous with its reaction to detecting
>> a compromised SECBOOT partition, and also had some inconsistencies in behavior.
>>
>> Case 1: SECBOOT partition cleared.
>> .init() checks the header for the magic number and version. As neither matches,
>> will reformat the entire partition. Now, .load_bank() will pass, as the data
>> was just freshly reformatted (note: this also could trigger the bug addressed
>> in the previous patch). Only variables in the TPM will be loaded by
>> .load_bank() as the data in SECBOOT is now empty.
>>
>> Case 2: Bank hash mismatch.
>> .load_bank() panics and returns an error code, causing secvar_main() to jump
>> to the error scenario, which prevents the secvar API from being exposed.
>> os-secure-enforcing is set unconditionally, and the user will have no API to
>> manage or attempt to fix their system without issuing a key clear request.
>>
>> This patch unifies the behavior of both of these cases. Now, .init() handles
>> checking the header AND comparing the bank hash. If either check fails, the
>> SECBOOT partition will be reformatted. Variables in the TPM will still be
>> loaded in the .load_bank() step, and provided the backend stores its
>> secure boot state in the TPM, secure boot state can be preserved.
>>
>> Signed-off-by: Eric Richter <erichte at linux.ibm.com>
>> Tested-by: Nick Child <nick.child at ibm.com>
>> ---
>> libstb/secvar/storage/secboot_tpm.c | 30 +++++++++++++++++++-
>> libstb/secvar/test/secvar-test-secboot-tpm.c | 6 ++++
>> 2 files changed, 35 insertions(+), 1 deletion(-)
>>
>> diff --git a/libstb/secvar/storage/secboot_tpm.c b/libstb/secvar/storage/secboot_tpm.c
>> index 5907ff07..932e5fd3 100644
>> --- a/libstb/secvar/storage/secboot_tpm.c
>> +++ b/libstb/secvar/storage/secboot_tpm.c
>> @@ -374,7 +374,9 @@ fail:
>> return rc;
>> }
>>
>> -static int secboot_tpm_load_variable_bank(struct list_head *bank)
>> +
>> +/* Helper to validate the current active SECBOOT bank's data against the hash stored in the TPM */
>> +static int compare_bank_hash(void)
>> {
>> char bank_hash[SHA256_DIGEST_LENGTH];
>> uint64_t bit = tpmnv_control_image->active_bit;
>> @@ -394,6 +396,15 @@ static int secboot_tpm_load_variable_bank(struct list_head *bank)
>> /* Tampered pnor space detected, abandon ship */
>> return OPAL_PERMISSION;
>>
>> + return OPAL_SUCCESS;
>> +}
>> +
>
> So in doing this you take the check out of secvar_ops.load_bank(). I am
> guessing that:
>
> - secboot_tpm_store_init() gets called before any calls to load_bank()
>
Correct.
> - there's nothing that skiboot does that might cause the contents of
> the PNOR to change between store_init() and load_bank()?
>
Correct.
> - anything that's able to change the PNOR while skiboot is running this
> early boot stuff is outside the threat model?
>
Correct.
> Is that right?
See secvar_main() in secvar_main.c for the flow. It is one call to _init,
then one call to _load_bank() for the variable bank, then the update bank.
Side note: the entire SECBOOT partition is loaded into memory in _init().
No further reads from PNOR are made, so even if PNOR is modified, it
won't be read until the next boot (assuming it doesn't get overwritten
by a write). load_bank() is more accurately "parse_bank()".
This is of course assuming memory can't be arbitrarily changed in this
time, but that would have even worse implications than this.
>
>> +
>> +static int secboot_tpm_load_variable_bank(struct list_head *bank)
>> +{
>> + uint64_t bit = tpmnv_control_image->active_bit;
>> + int rc;
>> +
>> rc = secboot_tpm_deserialize_from_buffer(bank, tpmnv_vars_image->vars, tpmnv_vars_size, SECVAR_FLAG_PROTECTED);
>> if (rc)
>> return rc;
>> @@ -692,8 +703,25 @@ static int secboot_tpm_store_init(void)
>> rc = secboot_format();
>> if (rc)
>> goto error;
>> + goto done;
>> }
>>
>> + /* Verify the active bank's integrity by comparing against the hash in TPM.
>> + * Reformat if it does not match -- we do not want to load potentially
>> + * compromised data.
>> + * Ideally, the backend driver should retain secure boot state in
>> + * protected (TPM) storage, so secure boot state should be the same, albeit
>> + * without the data in unprotected (PNOR) storage.
>
> Don't you have 3 reserved bytes in struct secboot_header? Could you use
> one and say it's only defined in the case of the tpmnv_control header?
> Or does this state already get preserved and I've just misunderstood the
> interplay between backends/storage drivers/whatever?
>
This is fancy wordplay since this is a storage driver that isn't supposed
to care about secure boot modes, states, and variables.
I'm specifically talking about the PK. If the PK is populated with a key,
we are in os-secure-enforcing. That is the "secure boot state" that is
being preserved across a PNOR wipe by being stored in TPM NV instead.
>> + */
>> + rc = compare_bank_hash();
>> + if (rc == OPAL_PERMISSION) {
>> + rc = secboot_format();
>> + if (rc)
>> + goto error;
>> + }
>> + else if (rc)
>> + goto error;
>> +
>> done:
>> return OPAL_SUCCESS;
>>
>> diff --git a/libstb/secvar/test/secvar-test-secboot-tpm.c b/libstb/secvar/test/secvar-test-secboot-tpm.c
>> index 798ca281..f203e57d 100644
>> --- a/libstb/secvar/test/secvar-test-secboot-tpm.c
>> +++ b/libstb/secvar/test/secvar-test-secboot-tpm.c
>> @@ -101,10 +101,15 @@ int run_test(void)
>>
>> clear_bank_list(&variable_bank);
>>
>> + /* Disabling check for now, since bank hash check has been moved to _init().
>> + * This will be re-enabled with the forthcoming rework of the tests/_init() procedure.
>> + */
>> +#if 0
>> // Tamper with pnor, hash check should catch this
>> secboot_image->bank[0][0] = ~secboot_image->bank[0][0];
>>
>> rc = secboot_tpm_load_bank(&variable_bank, SECVAR_VARIABLE_BANK);
>> + fprintf(stderr, "rc = %d\n", rc);
>> ASSERT(rc != OPAL_SUCCESS); // TODO: permission?
>>
>> // Fix it back...
>> @@ -113,6 +118,7 @@ int run_test(void)
>> // Should be ok again
>> rc = secboot_tpm_load_bank(&variable_bank, SECVAR_VARIABLE_BANK);
>> ASSERT(rc == OPAL_SUCCESS);
>> +#endif
>>
>
> I haven't go through the rest of the series but is the rework part of
> this series or not?
>
No. The test case rework is next up, but it is not as urgently needed.
Currently we are testing by hand the changes in this set.
> I think I conceptually prefer just deleting the code and leaving it in
> the git history but I don't _really_ care, especially for tests...
>
> Kind regards,
> Daniel
>
More information about the Skiboot
mailing list