Preventing An OpenPGP Smartcard From Caching The PIN Eternally

From LinuxReviews
Jump to navigationJump to search
System-lock-screen.png

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.

Original story by Louis-Philippe Véronneau. Published 2021-03-13, Originally published 2021-03-13.
This work is available under the Creative Commons Attribution-ShareAlike license.

Binary-security.jpg
Image credit: geralt/Pixabay.

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()
0.00
(0 votes)

Add your comment
LinuxReviews welcomes all comments. If you do not want to be anonymous, register or log in. It is free.