GPG-Agent Socket Paths

Written by Dominik Pantůček on 2024-12-19

pgppython

Originally working on a YubiKey support for Crypt4GH stream processing library turned out to become a work to fully support GnuPG at least on UNIX platforms. In recent GnuPG versions everything is handled by gpg-agent but an interesting problem arose - how to find its socket when using alternative GnuPG homedir?


Although we have implemented the Assuan wire protocol and now it is possible to use gpg-agent for computing the result of Elliptic Curve Diffie-Hellman key exchange on Curve25519, discovering the unix socket provided by the running agent turned out to be trickier than anyone anticipated.

The good news is that the socket is always named "S.gpg-agent". The bad news is that it is located in a directory that depends on three major factors:

  • filesystem hierarchy standard specifics of the running system
  • current user UID
  • (semi-)canonical path to GnuPG's directory with private keys

The first two points have to be handled together. There are four hardcoded variants in GnuPG sources and current user UID has to be inserted in the patch before checking its existence anyway. In any case the last element of the base path created is always GnuPG subdirectory. A simple Python function for finding the right base directory can look like this:

def compute_run_gnupg_base(
    bases: List[str] = ["/run/gnupg", "/run", "/var/run/gnupg", "/var/run"]
) -> str:
    """Computes possible gnupg's run directories and verifies their
    existence.

    Returns:
        The actual gnupg's run directory of current user.

    """
    uid = os.getuid()
    ubases = [f"{base}/user/{uid}" for base in bases]
    for ubase in ubases:
        if os.path.isdir(ubase):
            return f"{ubase}/gnupg"
    raise ArgumentError("Cannot find GnuPG run base directory")

Trouble is that this is the final directory if and only if GnuPG uses its default homedir - that is the directory with keyrings and other data. However it is possible to use GnuPG components backed by other homedir (and it is very useful in many scenarios). With alternative homedir, it is necessary to find a subdirectory in the base socket directory and use this subdirectory for everything. But how do we do that?

Firstly we need to compute a SHA1 hash of the semi-canonical name of the data directory in question. Then we need to take first 15 bytes of this hash and encode it in BASE32 as per RFC 6189. If it sounds like an odd decision to a cautious reader - do not feel bad, it definitely is at least an iteresting design choice.

However if we prepare transcoding table between RFC 4648 and RFC 6189 alphabets, the actual code for implementing the hashing and ecoding needed is relatively straightforward:

gen_b32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
gpg_b32 = "ybndrfg8ejkmcpqxot1uwisza345h769"

gpg_trans = str.maketrans(gen_b32, gpg_b32)

def compute_socket_dir_hash(path: str) -> str:
    """Computes partial message digest of given path to be used as
    shortened path component in the base socket directory path. The
    implemenation is compatible with gnupg's homedir.c and zb32.c as
    well as with libgcrypt's SHA1 message digest.

    Parameters:
        path: canonical (as understood by gnupg) path to the original directory

    """
    bpath = path.encode()
    md = sha1(path.encode()).digest()
    md15 = md[:15]
    b32 = b32encode(md15)
    s32 = b32.decode("ascii")
    z32 = s32.translate(gpg_trans)
    return z32

With the base directory and our newly computed homedir hash, the actual subdirectory is obtained by concatenating "d." and this encoded hash. This leaves us with the full path to gpg-agent socket directory.

def compute_socket_dir(homedir: str = None) -> str:
    """Computes the actual socket dir used by gpg-agent based on given
    homedir (the private key storage directory).

    If given directory is None, returns the root run directory for
    gnupg (as required by gpg-agent).

    Parameters:
        homedir: canonical path to the directory

    Returns:
        Socket base directory.

    """
    base = compute_run_gnupg_base()
    if homedir is not None:
        dhash = compute_socket_dir_hash(homedir)
        return f"{base}/d.{dhash}"
    return base

You can easily verify that by checking that the named pipe "S.gpg-agent" is available in this directory when gpg-agent is running and that you can also connect to it using our infamous Assuan protocol.

Once again we had to dug deeper than anticipated for this one, but it was hopefully nevertheless interesting. See ya next time for more!