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:
- R (ratelimit) allows you to check if a key is invalid.
- G (gambling) is a tripwire that permanently locks if given the duress passphrase. It’s implemented as an entry with 0 retries.
In broad strokes:
- The user types in the passphrase.
- The TPM answers the question of “is this either the real passphrase
or the duress passphrase?” with a rate limit.
- If it’s one of the two (the TPM doesn’t say which), the OS asks the
TPM “this is the real passphrase, right?”.
- If it wasn’t, the TPM bricks itself forever.
- If it was, the TPM returns a salt to use to unlock the disk.
Definitions
H(x[; d]): some HKDF with domain separation
KDF(x; s): some salted KDF
trunc(x; n): keep n lowest bits
tpm_blind(x; d): destructive hashing of a potentially sensitive value, i.e.H(trunc(H(x; d); n))
P_good,P_duress: Passphrases
A_R,A_G: AuthValue for TPM NV entries R and G
S_R,S_G: Salts stored by TPM NV entries R and G
MASK: 256-bit vector
Unlock flow (happy path)
- User enters a passphrase
P = P_good. - Software calculates
K = KDF(P; s). - Software calculates
A_R = tpm_blind(K; "tpm stage 1") & MASK. - Software asks to unlock TPM entry R with
A_R. - TPM returns salt
S_R. - Software calculates
A_G = tpm_blind(K || S_R; "tpm stage 2") - Software asks to unlock TPM entry G with
A_G. - TPM returns salt
S_G. - Software uses
H(K; S_G)to decrypt disk.
Unlock flow (duress path)
- User enters a passphrase
P = P_duress.
…
- TPM does not return a salt and increments failures on entry G by one.
S_Gwill never be released after this due to policy.
Provisioning
- Software generates dummy
ownerAuthandlockoutAuth, initializes TPM, and discards it. - User chooses two dissimilar passphrases
P_goodandP_duress. - Software generates salt for KDF, calculates
K_g = KDF(P_good; s)andK_d = KDF(P_duress; s), storesMASK = ~(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) - Software generates salts
S_RandS_G. - Software calculates AuthValues
A_RandA_G - TPM object
Ris created with value=S_Rand authvalue=A_R, protected by DA - TPM object
Gis created with value=S_Gand authvalue=A_G, authpolicy ofCounter = 0+AuthValue,PIN_FAILintoCounter - TPM object
Counteris created and initialized to 0, is thePIN_FAILofG