Preventing An OpenPGP Smartcard From Caching The PIN Eternally
GnuPG will happily cache the PIN for hardware tokens like security smartcards forever. GnuPG does have a
cache-ttl parameter, but it is not implemented so it does absolutely nothing. Debian developer Louis-Philippe Véronneau has a solution.
Image credit: geralt/Pixabay.
Security-wise, this is not great. Instead of manually disconnecting the token frequently, I've come up with a script that restarts scdameon if the token hasn't been used during the last X minutes.
It seems to work well and I call it using this cron entry:
*/5 * * * * my_user /usr/local/bin/restart-scdaemon
To get a log from scdaemon, you'll need a ~/.gnupg/scdaemon.conf file that looks like this:
debug-level basic log-file /var/log/scdaemon.log Hopefully it can be useful to others!
#!/usr/bin/python3 # Copyright 2021, Louis-Philippe Véronneau <email@example.com> # # This script is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # # This script is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this script. If not, see <http://www.gnu.org/licenses/>. """ This script restarts scdaemon after X minutes of inactivity to reset the PIN cache. It is meant to be ran by cron each X/2 minutes. This is needed because there is currently no way to set a cache time for smartcards. See https://dev.gnupg.org/T3362#137811 for more details. """ import os import sys import subprocess from datetime import datetime, timedelta from argparse import ArgumentParser p = ArgumentParser(description=__doc__) p.add_argument('-l', '--log', default="/var/log/scdaemon.log", help='Path to the scdaemon log file.') p.add_argument('-t', '--timeout', type=int, default="10", help=("Desired cache time in minutes.")) args = p.parse_args() def get_last_line(scdaemon_log): """Returns the last line of the scdameon log file.""" with open(scdaemon_log, 'rb') as f: f.seek(-2, os.SEEK_END) while f.read(1) != b'\n': f.seek(-2, os.SEEK_CUR) last_line = f.readline().decode() return last_line def check_time(last_line, timeout): """Returns True if scdaemon hasn't been called since the defined timeout.""" # We don't need to restart scdaemon if no gpg command has been run since # the last time it was restarted. should_restart = True if "OK closing connection" in last_line: should_restart = False else: last_time = datetime.strptime(last_line[:19], '%Y-%m-%d %H:%M:%S') now = datetime.now() delta = now - last_time if delta <= timedelta(minutes = timeout): should_restart = False return should_restart def restart_scdaemon(scdaemon_log): """Restart scdaemon and verify the restart process was successful.""" subprocess.run(['gpgconf', '--reload', 'scdaemon'], check=True) last_line = get_last_line(scdaemon_log) if "OK closing connection" not in last_line: sys.exit("Restarting scdameon has failed.") def main(): """Main function.""" last_line = get_last_line(args.log) should_restart = check_time(last_line, args.timeout) if should_restart: restart_scdaemon(args.log) if __name__ == "__main__": main()