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 <pollo@debian.org>
#
# 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()