latchset / clevis

Automated Encryption Framework
GNU General Public License v3.0
917 stars 104 forks source link

Support for TPM 2.0 Authorized Policies #121

Open dnoliver opened 5 years ago

dnoliver commented 5 years ago

Today, Clevis supports "static" PCR Policies through the TPM Pin. This makes using the TPM much more easier for regular users, replacing all the TPM2 Tools command with a single instruction for encryption and decryption.

Unfortunately, by using this strategy, a future update of binaries that affects the values of the PCR banks selected in the policy can cause the inability to unseal the secrets previously sealed to those PCRs. This is called PCR fragility (or PCR brittleness).

Fragility of PCR values was one of the most annoying problems with the 1.0 family of TPMs. Both keys and data can be locked to certain PCRs having particular values. But if keys or data are locked to a PCR that represents the BIOS of a system, it's tricky to upgrade the BIOS. Typically, before a BIOS upgrade was performed on TPM 1.2 systems, all secrets locked to PCR 0, for example (which represents the BIOS), had to be unsealed and then resealed after the upgrade was done. This is both a manageability nightmare and a nuisance to users.

In the TPM 2.0 specification, you can seal things to a PCR value approved by a particular signer instead of to a particular PCR value (although this can still be done if you wish). That is, you can have the TPM release a secret only if PCRs are in a state approved (via a digital signature) by a particular authority.

With the TPM2 Tools 4.0 release, Authorized Policies support is provided thorough the tpm2_policyauthorize command. This allows to setup a policy that can be updated in the future to react to system updates that affects the PCR. The process to use tpm2_policyauthorize is a little bit complex, but I think Clevis can replace that complexity as it already did with tpm2_policypcr.

The way to use Authorized Policies today is specified below in that long script. The script was tested against the following versions

NOTE: This is a test similar to the abrmd_policyauthorize.sh test in the tpm2-software/tpm2-tools repo (thank you @idesai for your help). It uses a simulator, and can be executed in a Docker container. It does not uses the resource manager, and that is why there are several tpm2_flushcontext --transient commands issued. The resource manager aware version would be a little shorter.

# Simulator Setup
tpm_server -rm > /dev/null 2>&1 &
TPM2_SERVER_PID=$!
sleep 1
export TPM2TOOLS_TCTI=mssim
tpm2_startup --clear

# Create a PCR policy
tpm2_startauthsession --session session.ctx
tpm2_policypcr \
    --session session.ctx \
    --pcr-list sha256:0,1 \
    --policy pcr.policy
tpm2_flushcontext session.ctx
rm -f session.ctx

# Generate public/private key pair for signing
openssl genrsa -out signing.priv.pem
openssl rsa \
    -in signing.priv.pem \
    -out signing.pub.pem \
    -pubout

# Loading the public key in TPM
tpm2_loadexternal \
    --key-algorithm rsa \
    --hierarchy o \
    --public signing.pub.pem \
    --key-context signing.key.ctx \
    --name signing.key.name

# Flush transient objects
tpm2_flushcontext --transient

# Create an authorized policy
tpm2_startauthsession --session session.ctx
tpm2_policyauthorize \
    --session session.ctx \
    --policy authorized.policy \
    --name signing.key.name \
    --input pcr.policy
tpm2_flushcontext session.ctx
rm -f session.ctx signing.key.ctx

# Create the TPM object that holds the secret
echo "Secret" > secret.txt
tpm2_createprimary \
    --hierarchy o \
    --hash-algorithm sha256 \
    --key-algorithm rsa \
    --key-context primary.ctx
tpm2_create \
    --parent-context primary.ctx \
    --hash-algorithm sha256 \
    --public key.obj.pub \
    --private key.obj.priv \
    --sealing-input secret.txt \
    --policy authorized.policy

# Flush transient objects
tpm2_flushcontext --transient

# Load the generated object in the TPM
tpm2_load \
    --parent-context primary.ctx \
    --public key.obj.pub \
    --private key.obj.priv \
    --name key.obj.name \
    --key-context key.obj.ctx

# Persist the TPM Object
tpm2_evictcontrol \
    --hierarchy o \
    --object-context key.obj.ctx \
    0x81010001

# Flush transient objects
tpm2_flushcontext --transient

# Sign the PCR policy
openssl dgst \
    -sign signing.priv.pem \
    -out pcr.policy.signature \
    pcr.policy

# Load the keys to be used in signature verification
tpm2_loadexternal \
    --key-algorithm rsa \
    --hierarchy o \
    --public signing.pub.pem \
    --key-context signing.key.ctx \
    --name signing.key.name

# Verify the signature
tpm2_verifysignature \
    --ticket verification.tkt \
    --key-context signing.key.ctx \
    --hash-algorithm sha256 \
    --message pcr.policy \
    --signature pcr.policy.signature \
    --format rsassa

# Flush transient objects
tpm2_flushcontext --transient

# Unseal the TPM secret using the Authorized PCR Policy
tpm2_startauthsession \
    --policy-session \
    --session session.ctx
tpm2_policypcr \
    --session session.ctx \
    --pcr-list sha256:0,1
tpm2_policyauthorize \
    --session session.ctx \
    --policy authorized.policy \
    --input pcr.policy \
    --name signing.key.name \
    --ticket verification.tkt
tpm2_unseal \
    --object-context 0x81010001 \
    --auth session:session.ctx
tpm2_flushcontext session.ctx
rm -f session.ctx

A negative test of this setup is shown below. In this case, the PCR 0 is updated, and the secret cannot be unsealed again. This will be the current behavior with the "static" PCR policies.

# Negative Test: after pcr extend unsealing should fail
tpm2_pcrextend 0:sha256=0000000000000000000000000000000000000000000000000000000000000000
tpm2_startauthsession \
    --policy-session \
    --session session.ctx
tpm2_policypcr \
    --session session.ctx \
    --pcr-list sha256:0,1
tpm2_policyauthorize \
    --session session.ctx \
    --policy authorized.policy \
    --input pcr.policy \
    --name signing.key.name \
    --ticket verification.tkt || echo 'Failed'
tpm2_flushcontext session.ctx
rm -f session.ctx

# Flush transient objects
tpm2_flushcontext --transient

And here, the update scenario, where a new valid signed policy is provided for unsealing:

# Update Test: Providing a new policy with updated values should work
tpm2_startauthsession --session session.ctx
tpm2_policypcr \
    --session session.ctx \
    --pcr-list sha256:0,1 \
    --policy newpcr.policy
tpm2_flushcontext session.ctx
rm -f session.ctx

# Update Test: Sign new policy with updated values
openssl dgst \
    -sign signing.priv.pem \
    -out newpcr.policy.signature \
    newpcr.policy

# Update Test: Get new verification ticket for the policy
tpm2_verifysignature \
    --ticket newverification.tkt \
    --key-context signing.key.ctx \
    --hash-algorithm sha256 \
    --message newpcr.policy \
    --signature newpcr.policy.signature \
    --format rsassa

# Update Test: Unseal with updated policy
tpm2_startauthsession \
    --policy-session \
    --session session.ctx
tpm2_policypcr \
    --session session.ctx \
    --pcr-list sha256:0,1
tpm2_policyauthorize \
    --session session.ctx \
    --policy authorized.policy \
    --input newpcr.policy \
    --name signing.key.name \
    --ticket newverification.tkt
tpm2_unseal \
    --object-context 0x81010001 \
    --auth session:session.ctx
tpm2_flushcontext session.ctx
rm -f session.ctx

Finally, some cleanup and shutdown instructions:

# Removing persistent handles
tpm2_getcap handles-persistent
tpm2_evictcontrol \
    --hierarchy o \
    --object-context 0x81010001

# Simulator Shutdown
kill ${TPM2_SERVER_PID}

All this could be replaced with something similar to this in clevis:

# Encrypt the  key with Clevis
cat key.txt | clevis encrypt \
    tpm2 '{"pcr_bank":"sha256","pcr_ids":"0", "authorized_policy": true, "signing_auth": "public.key"}' > key.jwe

# Decrypt the rsa key with Clevis, should work if PCRs are in expected state
clevis decrypt < key.jwe > key.txt

# Negative test
# Extend PCR 0 and attempt to unseal, should fail
tpm2_pcrextend 0:sha256=0000000000000000000000000000000000000000000000000000000000000000
clevis decrypt < key.jwe || echo 'Expected to Fail'

# Update test
# Provide a new authorized policy contemplating the update, should work
clevis decrypt tpm2 '{"policy": "authorized.policy" }' < key.jwe

# Update test
# Policy should be updated, and secret should be able to unseal
clevis decrypt < key.jwe > key.txt

Is this something you could implement?

Thank you!

dnoliver commented 5 years ago

Also reported in Red Hat Bugzilla https://bugzilla.redhat.com/show_bug.cgi?id=1762421

dnoliver commented 4 years ago

The bugzilla issue was closed with status "CLOSED NEXTRELEASE" There is another version of clevis-pin-tpm2 that seems to implement TPM2.0 Authorized Policies

nullr0ute commented 4 years ago

Yes, we have coordinated with the clevis team on this.

superm1 commented 4 years ago

@nullr0ute so is the intention that eventually that other clevis-pin-tpm2 gets merged into this branch?

nullr0ute commented 4 years ago

@nullr0ute so is the intention that eventually that other clevis-pin-tpm2 gets merged into this branch?

Still TBD. It's possible, it's written in rust so there's quite a difference in technology and the tpm2 module here will detect if it's present and it will use it automatically else fail back so in the short term it can evolve independently ATM

superm1 commented 4 years ago

I guess with regards to authorization policies though and making it easy to use them, clevis' framework would need to make some decisions. IE where to pick up keypairs to sign authorization policies, automatically authorizing them etc.

My main thought is around enrolling the systems fwupd key and so you can actually have a pcr policy tied to PCR0, but do a firmware update through fwupd the appropriate authorization policy is set up that you can boot the next time.

puiterwijk commented 4 years ago

I guess with regards to authorization policies though and making it easy to use them, clevis' framework would need to make some decisions. IE where to pick up keypairs to sign authorization policies, automatically authorizing them etc.

My thought wrt this was that Clevis would not sign the authorization policies, but that that's depending on the user. For example, companies with multiple systems can generate the policies at their central servers and then deploy it to machines in the field pre-firmware-upgrade.

My main thought is around enrolling the systems fwupd key and so you can actually have a pcr policy tied to PCR0, but do a firmware update through fwupd the appropriate authorization policy is set up that you can boot the next time.

That would be one way users could indeed hook it up, and probably ideal, but I did not do that integration yet.

dnoliver commented 3 years ago

I wanted to test this, but I don't have clevis-pin-tpm2-signtool in my system. For reference, the tool is located here https://github.com/puiterwijk/clevis-pin-tpm2-signtool, and it is necessary to create the signed policy.

Is this an expected behavior? or an packaging/install problem?

My info:

[root@fedora-iot ~]# rpm -qa clevis*
warning: Found bdb Packages database while attempting sqlite backend: using bdb backend.
clevis-15-2.fc33.x86_64
clevis-systemd-15-2.fc33.x86_64
clevis-pin-tpm2-0.2.0-1.fc33.x86_64
clevis-luks-15-2.fc33.x86_64
clevis-dracut-15-2.fc33.x86_64
[root@fedora-iot ~]# clevis
clevis                   clevis-decrypt-tang      clevis-encrypt-sss       clevis-encrypt-tpm2plus  clevis-luks-list         clevis-luks-unbind
clevis-decrypt           clevis-decrypt-tpm2      clevis-encrypt-tang      clevis-luks-bind         clevis-luks-regen        clevis-luks-unlock
clevis-decrypt-sss       clevis-decrypt-tpm2plus  clevis-encrypt-tpm2      clevis-luks-edit         clevis-luks-report       clevis-pin-tpm2
[root@fedora-iot ~]# 
ghost commented 2 years ago

Hey @dnoliver, have you sucessfully tested it ?