"Pining for GPG to try"

So anyone's who had to suffer through seeing one of my talks (and let's be honest, we can't get enough of them, can we?) you've have heard me talk lovingly about Yubikeys. They're hardware tokens that can store crytographic tokens for HOTP and a bunch of other stuff.

Why do we care? Well the expensive ones, the Neos and the Neo-N can act as CCID smart cards, which means you can run the gauntlet of generating GPG keys on them. Cool GPG, whoo &c. I know, I know. I use GPG all the time, but let's be honest @Moxie is right when he says it's a philosophical dead end, buuuuuuuuuut we can do something useful with it.

Because Yubikeys support generating RSA keys, and one of the key possibilities in GPG is an "Authentication key", we can generate a key on the Yubikey and use it for SSH! The joys of this is that the private key you generate on the key cannot be extracted (well you know, easily). So you can be fairly sure that someone authenticating with this key actually has the token, rather than has just copied ~/.ssh/id_rsa off somewhere. Now this isn't a discussion on how to generate those keys, as there's a tonne of pages already on that subject..

But anyway, you should do that. It limits your attack surface for SSH. just, you know, discovering SSH_AUTH_SOCKET, or going after ControlMaster sockets, or trojaning the ssh client... okay there's still lots.

So what the hell is this about?

Automating PINs in gpg-agent

When using 'gpg --card-edit' to generate a key, the gpg-agent pinentry will prompt you for the PIN on the device. You only get 3 attempts at that, before you have to enter the admin PIN to unlock it. This is all well and good, but when you want to mass generate keys, for say, your entire organisation, sitting there entering PINs is a pain. So I wanted to do that in Python.

I tried numerous gpg2 options, such as --batch --passphrase-fd X, but that will only work for the first PIN. Which is not ideal if you want to change them.

So in the end, I started down the path of making my own pinentry script to send the PINs that I specify, just when they're being generated.

You'd think this was easy to do.

I fell down a rabbit hole of looking as the Assuan protocol which is how gpg-agent and pinentry communicate. When through some violent Googling and kind assistance from @antifuchs, I found you can just fake the pinentry end somewhat and just specify what you want. Finding pinentry-emacs was a huge help here.

I tried making a script to send back a PIN each time and change it during the running of the script, but it executes a new invocation of the script every time. I also tried setting it via environment variables, but they're not exposed in the right place, because it's called via gpg-agent. Madness.

In the end, I went via a horrible horrible hack of having a state file, which is updated as the script runs. Check out pinentry-hax.

Then tell gpg-agent to use that pinentry, and tell the agent to reload it's config.

echo "pinentry-program pinentry-hax" >~/.gnupg/gpg-agent.conf && echo RELOADAGENT | gpg-connect-agent

Now you have to have something actually writing out the "$TMP/.some_pretend_ipc" file, with the right things in it. Here's the extract from the Python script I'm using

def do_a_pin(ipc_file, oldpin, newpin):
    """
    for changing pins, or actually for generating keys (where
    oldpin and newpin are actually pin and adminpin)
    """

    # Yup, this is happening. A FILE BASED IPC METHOD.
    if os.path.exists(ipc_file):
        os.unlink(ipc_file)  # safety delete.

    old_umask = os.umask(0o077)  # safety umask!
    try:
        with open(ipc_file, 'w') as f:
            f.write('round=0\n')
            f.write("oldpin=%s\n" % oldpin)
            f.write("newpin=%s\n" % newpin)
    finally:
        os.umask(old_umask)

    return True

Then, when you call gpg, and it asks for the password, you can specify them with that function and then pinentry will supply them to GPG when asked. Why is this useful? Because then when you send commands to gpg, you can generate the thing as a whole.

pin=1234
adminpin=12345
do_a_pin('/tmp/.some_pretend_ipc',pin, adminpin)

in_fd, out_fd = os.pipe()
cmd = 'gpg2 --command-fd {fd} --card-edit'.format(fd=in_fd)

with Popen(shlex.split(cmd), pass_fds=[in_fd], stdin=PIPE) as p:
   os.close(in_fd)  # unused in the parent

   with open(out_fd, 'w', encoding='utf-8') as command_fd:
        command_fd.write("""commands to send to GPG, that ask for a PIN""")

The result is you can, from a python script, with some terrible hacking, automate generating RSA keys on Yubikeys, without having to sit there running every command and typing in every PIN.

I hope this is useful for someone. If there's a more elegant solution, I'd realllllllly love to hear it. Drop me an email at blog@thedomainofthisblog, or shout at me on the Twitters at @benjammingh.

Comments !

links

social