Duress passphrase with untrusted TPM (design)

Goal: GrapheneOS-style “duress PIN” for LUKS, even if the adversary can restore disk images.

Caveat: The TPM might be compromised. In that case, it should have the same properties as regular LUKS (duress trivial to bypass, TPM can’t decrypt alone).

Summary

TPM has two entries, R and G:

In broad strokes:

Definitions

Unlock flow (happy path)

  1. User enters a passphrase P = P_good.
  2. Software calculates K = KDF(P; s).
  3. Software calculates A_R = tpm_blind(K; "tpm stage 1") & MASK.
  4. Software asks to unlock TPM entry R with A_R.
  5. TPM returns salt S_R.
  6. Software calculates A_G = tpm_blind(K || S_R; "tpm stage 2")
  7. Software asks to unlock TPM entry G with A_G.
  8. TPM returns salt S_G.
  9. Software uses H(K; S_G) to decrypt disk.

Unlock flow (duress path)

  1. User enters a passphrase P = P_duress.

…

  1. TPM does not return a salt and increments failures on entry G by one.
  2. S_G will never be released after this due to policy.

Provisioning

  1. Software generates dummy ownerAuth and lockoutAuth, initializes TPM, and discards it.
  2. User chooses two dissimilar passphrases P_good and P_duress.
  3. Software generates salt for KDF, calculates K_g = KDF(P_good; s) and K_d = KDF(P_duress; s), stores MASK = ~(tpm_blind(K_g; "tpm stage 1") ^ tpm_blind(K_d; "tpm stage 1")) and the salt in LUKS header. (Optionally, 1-bits in MASK are flipped to 0 until popcount=2*n)
  4. Software generates salts S_R and S_G.
  5. Software calculates AuthValues A_R and A_G
  6. TPM object R is created with value=S_R and authvalue=A_R, protected by DA
  7. TPM object G is created with value=S_G and authvalue=A_G, authpolicy of Counter = 0 + AuthValue, PIN_FAIL into Counter
  8. TPM object Counter is created and initialized to 0, is the PIN_FAIL of G