While I'm overall very happy about my migration to an OpenPGP hardware token, the process wasn't entirely seamless and I had to hack around some issues, for example the PIN caching behavior in GnuPG.
As described in this bug the
cache-ttl parameter in GnuPG is not
implemented and thus does nothing. This means once you type in your PIN, it is
cached for as long as the token is plugged.
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()