Preventing an OpenPGP Smartcard from caching the PIN eternally

2021-03-13 - Louis-Philippe Véronneau

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!


# Copyright 2021, Louis-Philippe Véronneau <>
# 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 <>.

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 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:, os.SEEK_END)
        while != b'\n':
  , 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
        last_time = datetime.strptime(last_line[:19], '%Y-%m-%d %H:%M:%S')
        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."""['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:

if __name__ == "__main__":