Release a TPM-sealed secret when your face unlocks the machine.
Face auth proves who you are but produces no password. Anything downstream that
needs one — KDE Wallet, GNOME Keyring, an encrypted home — finds
PAM_AUTHTOK empty and prompts anyway. You get recognised by your face and
then asked for a password, which rather defeats the point.
This module fixes that: the face match authorises the TPM to release a real secret, which is handed to the rest of the PAM stack as if you had typed it.
The obvious idea — hash the face encoding, use it as an encryption key — does not work, and it is worth knowing why before dismissing it.
Face encodings are fuzzy. Six enrolments of the same face, captured seconds apart on the same camera, measured on the author's own model:
encoding 0 vs 1 : 0.0829
encoding 0 vs 4 : 0.3609
encoding 1 vs 4 : 0.3846
encoding 2 vs 5 : 0.3166 (identical vectors would be 0.0000)
That is why matching uses a distance threshold rather than equality. A hash of one encoding is unrelated to a hash of the next, so there is nothing stable to derive a key from. And a face is not secret in the first place — your model file sits on disk next to the data it would be protecting.
So the biometric authorises; the TPM holds. That is the only arrangement where "unlock with your face" is not a synonym for "keep the password in a file".
Windows uses the same pattern, though in two independent places that are easy
to conflate: BitLocker has the TPM release the volume key at boot, from PCR
measurements, before any user exists — Hello is not involved. Hello separately
has the TPM release your credentials after a biometric match, once you are at
the login screen. This module is the second of those. The first is
systemd-cryptenroll --tpm2-device=auto, and the two compose.
Protects against drive theft. The TPM will not unseal on other hardware. Pull the SSD, put it in another machine, get nothing. If your disk is not otherwise encrypted, this is the difference between "my browser passwords are readable by anyone who picks up the laptop" and "they are not".
Optionally protects against boot tampering — bind to PCR 7 and the secret stops unsealing if Secure Boot state changes.
Does not protect against anyone using your machine while it is running and logged in, or anyone who can already run code as root. Nothing at this layer does.
Not a substitute for full-disk encryption, and not competing with it. FDE protects far more, and if you can run it you should — the two compose fine.
The honest difference is cost of adoption, not safety:
| TPM-backed FDE | this module | |
|---|---|---|
| protects | the whole disk at rest | one secret |
| to adopt on an existing unencrypted system | backup + reinstall, or an involved in-place conversion | install, enrol, two PAM lines |
| if the TPM is cleared | you type your existing passphrase at boot | you type your password at the keyring prompt |
Both are recoverable when the TPM is cleared. systemd-cryptenroll --tpm2-device=auto adds a keyslot; your passphrase slot survives unless you
explicitly --wipe-slot=password. (Windows does the same — BitLocker keeps a
recovery key, escrowed to your account, and Windows Update suspends protection
around firmware updates so PCRs can change without stranding you.)
So this is not "safer than FDE". It is cheap, additive, and solves a different problem: session secrets, not the disk. If your root is already unencrypted and you are not about to reinstall, this gets you face-unlocked keyrings today.
Howdy is not presentation-attack-resistant. An IR sensor raises the bar over an RGB webcam, but this is not a certified biometric stack. Decide what you seal accordingly.
- A TPM 2.0 — check with
systemd-analyze has-tpm2 - systemd built with TPM2 support (provides
systemd-creds) - Howdy, or any PAM module that authenticates without producing a password
libpamheaders to build
No tpm2-tools needed.
make
sudo make installInstalls pam_howdy_tpm.so to /usr/lib/security/ and howdy-tpm-enroll to
/usr/bin/. Installing does nothing on its own — the module is inert until
you add it to a PAM stack.
sudo howdy-tpm-enrollPrompts twice, hidden. The secret goes straight to systemd-creds — never to a
file, never into argv, never into shell history. Sealed to
/etc/howdy-tpm/secret.cred, root:root 600, and read back before success is
reported.
sudo howdy-tpm-enroll --show # status; never prints the secret
sudo howdy-tpm-enroll --remove # delete it; PAM falls back to prompting
sudo howdy-tpm-enroll --pcrs 7 # additionally bind to Secure Boot stateMultiple secrets are fine — give each its own name and path:
sudo howdy-tpm-enroll --name ssh-key --cred /etc/howdy-tpm/ssh.credPlacement matters, and the obvious placement is wrong.
Put it where the auth modules converge on success — the target of
pam_howdy's success=N jump — and before whatever consumes the token:
auth requisite pam_faillock.so preauth
auth [success=N default=ignore] pam_howdy.so
auth [success=1 default=bad] pam_unix.so try_first_pass nullok
auth [default=die] pam_faillock.so authfail
auth optional pam_permit.so
auth optional pam_howdy_tpm.so <-- here
auth required pam_faillock.so authsucc
auth optional pam_kwallet5.so
Immediately after pam_howdy would be wrong: on success Howdy jumps over
the following modules, so a module placed there runs only when face auth
failed — exactly backwards.
Adding a module shifts every success=N below it. Recount them; do not eyeball
it. tools/flatten-pam.py prints the flattened stack with the jump targets
resolved.
| option | default | meaning |
|---|---|---|
cred=PATH |
/etc/howdy-tpm/secret.cred |
sealed credential |
name=NAME |
howdy-tpm |
credential name (must match enrolment) |
debug |
off | log decisions to syslog — never the secret |
Iterating on an auth stack by rebooting is how people lock themselves out. Don't.
tests/pamtest.c runs a real PAM conversation against any service and answers
no prompts, so a stack that still needs a password fails visibly:
gcc -o pamtest tests/pamtest.c -lpam
sudo ./pamtest login "$USER"A working face-only stack prints the Howdy messages and Success with no
Password: prompt. If Password: appears, something downstream still wants
one.
To prove the token is actually delivered, use pam_exec as an observer — see
tests/.
Failed tests increment faillock. Reset between batches:
sudo faillock --user "$USER" --resetIt never authenticates. pam_sm_authenticate returns PAM_IGNORE on every
path, including success. It supplies a token; it does not decide who may log
in. Returning PAM_SUCCESS would let a readable credential file substitute for
authentication.
It never overwrites an existing token. If PAM_AUTHTOK is already set —
normally by pam_unix on the password path — it does nothing. One placement
therefore serves both paths without knowing which ran.
It refuses an unsafe credential. Not root-owned, or any group/other permission bits, and it declines and logs. The file's contents become an authentication token downstream; if a non-root user can write it, that user chooses your wallet password.
It strips exactly one trailing newline. systemd-creds is byte-exact, so a
credential sealed with echo carries a \n that would travel into
PAM_AUTHTOK and make every unlock fail while looking exactly like a wrong
password. howdy-tpm-enroll uses printf '%s' for the same reason.
It shells out to systemd-creds rather than linking libsystemd: one
well-defined binary, the secret crosses a pipe we control, no in-process crypto
state. Buffers are wiped with explicit_bzero after use.
A TPM-sealed secret is recoverable only by this TPM, on this machine. Clear the TPM — firmware update, BIOS reset, "Clear TPM" in setup, moving the drive — and the blob is permanently undecryptable.
That is the entire security value, and the entire risk.
Write down whatever you seal and keep it somewhere that is not this laptop.
sudo howdy-tpm-enroll --show reports whether the credential still unseals, so
you can find out deliberately rather than at a login screen.
Working. Verified on a Dell XPS 16 (Panther Lake, STMicro TPM 2.0, kernel 7.2-rc6) with Howdy on an IR sensor, cosmic-greeter, and the GNOME login keyring.
A real boot, from the journal:
pam_howdy: Login approved
gkr-pam: stashed password to try later in open session
pam_unix(login:session): session opened for user jakes
stashed password is the keyring receiving the token. On every boot before
this, that line was pam_kwallet5: Couldn't get password (it is empty) and the
greeter sat waiting for a password it had already been told it did not need.
Face at the greeter, straight to the desktop, keyring unlocked, nothing typed.
Also verified, in an isolated PAM service rather than by guessing:
| case | result |
|---|---|
| no existing token | secret delivered, correct length |
| token already set | left alone — already set, doing nothing |
| credential mode 0644 | refused, no token set |
| credential owned non-root | refused |
| credential missing | no token, no crash |
Not yet done: an installer that edits PAM stacks for you. Deliberately — that
is the part that can lock you out. howdy-tpm doctor prints the exact lines
for your machine and you add them yourself.
MIT.