pinpam is a PAM module and credential utility to enable system-wide authentication with a secure TPM2-backed pin.
See this blog post for cool information and some insights into the design decisions:
- v0.0.5 :
- Experimental support for master-key, seperate optional feature allowing keyring integration and automatic unlocking regardless of auth method! This is a seprate pam module and subproject, and is entirely optional
- Large refactoring, reduction in unsafe code surface (it's still needed for TPM methods using unsafe C FFI and PAM using unsafe C FFI)
- Fix minor uid overflow bug that could result in errors.
- Add AI generated slovak translations to existing human ones. (improvements welcome)
- Add automatic sandboxing override for arch linux pkgbuild
- swtpm based integration testing.
- Fix minor security issue where RUST_LOG would allow enumeration of tpm general details (not key data) by unprivileged user.
- Improve comments and update documentation.
- Updates to nix flake format.
- Note: though the diff is large, most additions were to the test suite and the new pam master-key module.
- v0.0.4 :
- skiselkov (6): Add machine readable output/input, and Slovak language support, localization, and various cleanups.
- RazeLighter777 (3): Fix leading zero pin truncation (with migration for old format), update README.md, remove PTY usage from PAM module, bump versions, add version field to avoid future migration issues.
- v0.0.3 : fix policy access right TOCTOU (credit to nbdd0121), add landlock sandboxing, disallow ./policy as policy source.
Future pull requests should be made to merge to the dev branch, as the master branch will be reserved for tagged releases. This is to give better quality assurance before releases are made, and allow time for changes to be tested to prevent breakage and data loss
- Hardware-backed brute force protection
- Configurable number of allowed authentication failures.
- PIN resets
- NixOS flake with pam and udev configuration options.
- AUR (Arch User Repository) package.
- What does this program do? : pinpam lets you use a pin to authenticate yourself on linux. This could be for logging in, sudo, or any other service supported by PAM (pluggable authentication modules).
- How is this different than setting my password to a number (and using faillock)? : pinpam stores your pin in the TPM rather than in /etc/shadow. Storing a pin in /etc/shadow is a bad idea, if that file gets leaked, depending on the length of the pin, it can be trivial to brute force and reuse those credentials on another system. pinpam protects against hash dumping attacks and credential reuse.
- How do I reset/change a pin? : User's can change their own pins if they haven't been locked out with the pinutil command. A locked out pin must be manually reset by root.
- Isn't a pin less secure than a password? : It depends. Generally a pin is less secure than a strong password, but they can be more convenient and easier for users to embrace. You should consider your threat model when implementing any authentication service.
- Can I set a lockout duration? : You cannot at this time. I wanted this feature, but TPM2 afaik doesn't support this with pinfail indexes. Global dictionary attack does, but this would get rid of per user lockouts. If you have ideas on how this can be implemented please open up an issue.
- Will changing the lockout policy file affect existing pins? : No, users must change their pins to reload a new lockout policy. Admins can accomplish this by deleting all user pins.
- Can you support OTP? : I'd like to and this is a subject of research for me. Pull requests are welcome.
- License? : This project is licensed under the GPLv3.
- Packaging? : Currently this project is only in a nixOS flake and an AUR (arch user repository) package. You can manually build it and install the binaries if you wish, it should be broadly compatible. Pull requests welcome.
- Polkit support? : Polkit support should work OOTB for NixOS with the
enablePolkitPinoption. Currently on other distributions, polkit sandboxing break may break access to the tpm device and requires manual intervention. For a configuration fix see: this comment.
pinpam consists of two components:
- A PAM module (
libpinpam.so) exposing authentication functionality to PAM-aware applications. - A command-line utility (
pinutil) to setup/reset/change/manage PINs.
The PINs are stored in the TPM's NVRAM, protected by the TPM's hardware-backed security features. Upon creation, the PIN reset/attempts counter is marked read-only, preventing resetting the brute-force protection without clearing the TPM. This makes it difficult for an attacker to brute-force the PIN, as the TPM will lock out further attempts after a configurable number of failures. Even root will be unable to bypass this protection without clearing the TPM, which would also delete the stored PIN.
This module uses the little-known PinFail index data structure in the TPM 2.0 specification to track failed authentication attempts. This data structure is a simple counter/max-failures pair that is incremented by the TPM on each failed authentication attempt. When the maximum number of failures is reached, the TPM will refuse further authentication attempts until the counter is reset.
However, an attacker with root access could enumerate users pins and recover them by rewriting the PinFail index to reset the failure counter while making repeated authentication attempts. To mitigate this, pinpam uses a TPM2 policy to restrict the PinFail index to only being written once.
See SECURITY.md for a summary of the pinpam threat model
⚠️ ⚠️ ⚠️ Ensure that no user on the system other than root has direct access to the TPM device (e.g., /dev/tpm0 or /dev/tpmrm0). Direct access would allow users to delete/reset other users' pins, bypassing pinpam's security features.- A TPM2 (Trusted Platform Module) is required.
- No not give user's access to the tpm device, or they could delete/reset (but not read or brute force) other user's pins
- Losing access to the TPM (or clearing it) will result in the loss of the stored PIN and any associated data.
- You cannot reset a lockout without clearing the pin. This is a security feature to prevent brute-force attacks.
⚠️ Ensure you know what you are doing before marking pinpam asrequiredin PAM configurations. Lockout could prevent legitimate access to the system and opens a risk of denial of service attacks.sufficientwith a fallback method (e.g., regular unix auth) is recommended for most use cases.- pinutil is designed to operate as a setgid/setuid binary. If setgid is used, should be set to a group with rw access to /dev/tpmrm0 (e.g.,
tpmortss), assuming udev rules are set up correctly. See the NixOS flake for an example, which does this automatically, or the manual installation instructions. - A bug was fixed in 0.0.4 which incorrectly truncated leading zeros from pins. An automatic migration was put in place for this case, but note that if you use a new version of pinutil, your PIN will no longer work with the old version and will require either resetting or using the new tool.
TPM PIN authentication utility
Usage: pinutil [OPTIONS] <COMMAND>
Commands:
setup Set up a new PIN (root or user for self)
change Change PIN (requires current PIN, or root)
delete Delete PIN (requires PIN auth for non-root, root can delete any)
test Test PIN authentication
status Show PIN status
help Print this message or the help of the given subcommand(s)
Options:
-v, --verbose
-m, --machine Forces machine-readable output in JSON format and disables displaying input prompts. If not provided, machine mode is automatically enabled if stdin is NOT a terminal
-h, --help Print help
-V, --version Print version
Configuration file must be named policy. pinpam checks /etc/pinpam/policy. For security, it MUST be owned by root and have permissions 0644 or less Example policy file:
pin_min_length=4
pin_max_length=6
pin_lockout_max_attempts=5
pinutil_path=/nix/store/p2799cpnhk2malpmp7ilqvxg76gajlh9-pinpam-0.1.0/bin/pinutil
tcti=device:/dev/tpmrm0
Where
pin_min_length = minimum length of pin
pin_max_length = maximum length of pin
pin_lockout_max_attempts = number of allowed failed attempts before lockout
pinutil_path = path to pinutil binary to prevent path overwrite attacks. (mandatory)
tcti = optional TCTI spec naming the TPM backend (defaults to device:/dev/tpmrm0). Any string accepted by tss-esapi's TctiNameConf::from_str works, e.g. device:/dev/tpm0, tabrmd:bus_name=com.intel.tss2.Tabrmd, swtpm:host=127.0.0.1,port=2321, or mssim:host=127.0.0.1,port=2321.
libpinpam.so accepts the two standard password-stack arguments described in
pam.conf(5) / pam_unix(8):
try_first_pass— Before prompting for a PIN, try the authentication token cached by an earlier module in the stack (e.g.pam_unix). The cached token is only fed to the TPM if it actually parses as a valid PIN under the configured policy; non-digit strings or strings outsidepin_min_length/pin_max_lengthare skipped without consuming a TPM attempt. If the cached token is a valid PIN but the TPM rejects it, the user is then prompted interactively for the remainder of the allowed attempts.use_first_pass— Force the module to authenticate exclusively with the cached token from the previous stack entry; never prompt. If no token is cached, or the token is not a valid PIN, or the TPM rejects it, the user is denied. This is the right choice when you want a single combined prompt (e.g. one password field that doubles as a PIN entry behind an earlier module).
Both arguments are off by default. When neither is set, pinpam ignores any cached authtok and always prompts for the PIN itself, which is the historical behaviour.
Example: configure pinpam to silently piggy-back on the password the user already typed for the preceding module, prompting only if that token didn't parse as a PIN:
auth sufficient libpinpam.so try_first_pass
Example: configure a single-prompt sudo stack where the user types one number
that both pam_unix and pinpam see:
auth [success=ok default=ignore] pam_unix.so nullok
auth sufficient libpinpam.so use_first_pass
auth required pam_deny.so
The pinpam-core crate ships an end-to-end test suite that drives the TPM
through swtpm. The suite focuses on backward compatibility: once a PIN has
been provisioned by any released version, it must remain verifiable. Tests
gracefully skip when swtpm is not on PATH.
# from a checkout, with swtpm installed (the devShell provides it)
cargo test -p pinpam-core --test swtpm_pin
You will need to have Rust and Cargo installed. You will also need the TPM2 development libraries installed (e.g., tpm2-tss-dev on Debian-based systems) and the clang tools installed.
To build pinpam, clone the repository and run:
cargo build --release
First, ensure that a group exists that has access to the tpm device (e.g., tss or tpm), and that your user(s) are NOT members of that group. You can use udev rules to set the group ownership and permissions of the tpm device.
Place the resulting libpinpam.so in your PAM module directory (e.g., /lib/security or /lib64/security), and the pinutil binary in a directory of your choice (e.g., /usr/local/bin).
Add the pinpam PAM module to your desired PAM configuration files (e.g., /etc/pam.d/common-auth), taking care to configure it based on your needs and threat model.
Create a policy file as described above and ensure it is owned by root with permissions 0644
Then pick one of the two methods here to configure pinutil to access the TPM.
Set the pinutil binary to be setgid owned by a group with access to the tpm device through group permissions.
sudo groupadd tss (if it doesn't exist)
chgrp tss /path/to/pinutil
chmod g+s /path/to/pinutil
Then add a new file to /etc/udev/rules.d with these contents:
# TPM device access for tss group
KERNEL=="tpm[0-9]*", TAG+="systemd", MODE="0660", GROUP="tss"
KERNEL=="tpmrm[0-9]*", TAG+="systemd", MODE="0660", GROUP="tss"
Alternatively, you can simply add the setuid bit to pinutil with
chmod u+s /path/to/pinutil.
The pinpam project includes a NixOS flake that can be used to easily configure pin pam on a NixOS system.
First, add pinpam as an input to your flake:
{
inputs.pinpam.url = "github:razelighter777/pinpam";
}Then, enable pinpam in your NixOS configuration:
{
lib,
pkgs,
inputs,
config,
...
}:
let
cfg = config.my.pinpam;
in
{
imports = [ inputs.pinpam.nixosModules.default ];
config = lib.mkIf cfg.enable {
# Pinpam-specific configurations can go here
security.pinpam = {
enable = true;
enableTpmAccess = true;
enableSudoPin = true;
enableSystemAuthPin = true;
enableLoginPin = true;
enableHyprlockPin=true;
enablePolkitPin=true;
enableKdePin=true;
pinPolicy = {
minLength = 4;
maxLength = 6;
maxAttempts = 5;
};
};
};
}Notable toggle options under security.pinpam:
enableSystemAuthPin: Inserts pinpam as asufficientmodule for thesystem-authPAM stack so services that reusesystem-authaccept TPM PINs.enableLoginPin: Adds pinpam as asufficientrule to theloginPAM service for console logins.enableSudoPin: Enables PIN authentication within the sudo PAM stack.enableHyprlockPin: Enables PIN authentication for the Hyprlock PAM service when available.enablePolkitPin: Enables PIN authenticaion for polkit and configures polkit sandboxing.enableTpmAccess: Configures groups and udev rules needed to run pinutilenableKdePin: Enables PIN authentication for the kde service (works for kdescreenlocker)
pinpam can unlock a userspace secret store (KWallet, GNOME Keyring, pam_gnupg,
…) automatically at login, even when the user logs in with a PIN instead of
their password. A keyring's PAM module normally captures the login password from
PAM_AUTHTOK and uses it as the wallet key — but a PIN login never provides that
password, so the wallet stays locked.
The libpinpam_master_key.so module bridges this gap. After the auth decision
has been made, it overwrites PAM_AUTHTOK with a stable, TPM-bound per-user
token:
token = hex(HMAC(tpm_master_key, username))
A downstream keyring module then captures that token as the wallet key. Because the token depends only on the username and a TPM-resident master key — not on the login method — the wallet is keyed identically whether the user logged in with a PIN or a password. (You re-key the wallet to this token once; see your keyring's docs for changing the wallet password.)
libpinpam_master_key.so is a side-effect module, not an authenticator. Its
pam_sm_authenticate returns PAM_IGNORE on every path — success, failure,
or error — so it can never satisfy (or block) the auth decision regardless of
the control value an admin gives it. The worst case of any internal error is
that the wallet simply does not auto-unlock; a login is never granted or denied
because of this module. This is deliberate defence-in-depth against
misconfiguration.
Two ordering constraints follow from this:
- It must run after the real auth decision, so the wallet is only unlocked for genuine logins (the keyring opens the session only on overall success).
- It must run before the keyring module captures
PAM_AUTHTOK, and after any module (such as nixpkgs'unix-early) that would otherwise overwrite the token first.
The tricky part is that the common auth methods are sufficient: on success
pam_unix (or pinpam) short-circuits the whole stack, skipping the master-key
and keyring stages entirely. The robust fix is to move the decision into its own
substack. A sufficient short-circuit inside a substack is confined to the
substack — it does not short-circuit the parent — so the parent can still run
the master-key and keyring stages afterwards.
Hand-written /etc/pam.d example (works with pam_unix and any sufficient
auth method). Replace the module paths with wherever pinpam is installed on your
distribution (e.g. /lib/security or /usr/lib/security). First, the decision
substack, /etc/pam.d/pinpam-decide:
# PIN-or-password decision. Each method is sufficient; its short-circuit stays
# confined to this substack. The trailing deny fails the substack (and thus the
# parent) only if every method above failed.
auth sufficient libpinpam.so try_first_pass
auth sufficient pam_unix.so try_first_pass likeauth
auth requisite pam_deny.so
Then reference it from the real service, e.g. /etc/pam.d/sudo:
auth substack pinpam-decide
auth optional libpinpam_master_key.so
auth optional pam_kwallet5.so
# ... your usual account / password / session lines ...
On success the substack returns success without short-circuiting the parent, so
master-key stamps the wallet token and pam_kwallet5 captures it. On failure
the substack's requisite pam_deny fails the substack, and since nothing in the
parent then succeeds, auth is denied — no terminal pam_deny is needed (or
wanted) in the parent, because a substack cannot short-circuit it on success.
The flake's NixOS module generates exactly this layout for you. Enabling
security.pinpam.masterKey for a service writes a pinpam-decide-<service>
substack, disables the inline success methods and the parent's terminal deny,
and reorders the keyring rules to run right after the master-key stage:
{
security.pinpam = {
enable = true;
auth = {
enable = true;
services = [ "sudo" "login" ];
};
masterKey = {
enable = true;
services.login = {
enable = true;
# Auth methods whose success should route into the master-key stage.
# Each is copied into the decision substack as `sufficient` and disabled
# in the parent stack. List exactly the methods you use; add network /
# identity methods (e.g. "systemd_home", "ldap", "sss") here too.
successRules = [ "pinpam" "unix" ];
# Keyring rules moved to run after master-key so they capture the
# stamped token. Defaults to the common keyring/secret modules.
postAuthRules = [ "kwallet" "gnome_keyring" "gnupg" ];
};
};
};
}Relevant options under security.pinpam.masterKey:
enable: build and installlibpinpam_master_key.so.control: PAM control for the master-key line itself (defaultoptional; the module returnsPAM_IGNOREregardless).fallbackDenyOrder: order used to anchor the injected rules when the service's deny rule order can't be read (default13700).services.<name>.enable: turn on the master-key flow for a PAM service.services.<name>.successRules: see the comment above. Note the module cannot read a rule's originalenablewhile disabling it, so a name listed here is always copied into the substack — list only methods you actually use.services.<name>.postAuthRules: keyring/secret rules reordered after master-key. Names absent from the service become harmless disabled stubs.services.<name>.denyAnchorRule: existing rule used as the ordering anchor (default"deny").
This package is also available in the AUR in the package pinpam-git, authored by raze_lighter777 (me). You will need to manually configure the polkit service, as seen here: #4
Special thanks to creators of rust-tss-esapi, the foundation of this utility, and all other tirelessly hardworking open source maintainers that made this project possible