WireGuard Key on an OpenPGP Card

This article will show you how to use a WireGuard private key stored on an OpenPGP card, using the OpenPGP Card WireGuard Go client and OpenPGP Card X25519 Agent.

The OpenPGP Card WireGuard Go project is a small, experimental set of patches to the standard WireGuard Go client that can interact with your WireGuard private keys by way of an SSH agent interface, using a few custom SSH agent extensions. The OpenPGP Card X25519 Agent project is a minimal SSH agent that implements those extensions, plus the standard SSH agent ability to cache your OpenPGP card PIN.

For brevity, we’ll refer to the OpenPGP Card WireGuard Go software as the “OWG client”, and the OpenPGP Card X25519 Agent software as the “OX agent”. The OpenPGP Card X25519 Agent project also includes a separate client executable that can be used to interact with the OX agent (to set and clear the card PIN on the agent) — we’ll call that client the OX client.

WireGuard Private Key on an OpenPGP Card

When used together, the OWG client and OX agent allow you to keep a WireGuard private key stored safely on an OpenPGP card, without ever loading the key into your computer’s memory or disk. An OpenPGP card, in fact, has constraints that prevents it from ever exposing the actual private keys stored on the card — it only allows a few discrete operations to be performed using a private key through its hardware interface, without revealing the private key itself.

Therefore, if you generate a WireGuard key on an OpenPGP card (instead of importing an existing key generated elsewhere), you’re guaranteed that this key can never be stolen by an adversary remotely for use at a different time on a different computer. The key can be used only when the card is physically plugged into a computer, available to perform the periodic cryptographic operations required to initiate and maintain a WireGuard connection. Whoever controls the card physically, controls the key and its use.

This guide will cover everything you need to know to install and use both client and agent, in the following sections:

Requirements and Limitations

OpenPGP Card

First, you need an OpenPGP card with Curve 25519 support. The following cards are known to work with this system:

The OpenPGP card interface allows for only one master PGP key per card, with up to three subkeys:

  1. A signature (aka S or SIG) key, used to sign and verify messages. This technically can also be used for signature-based authentication, but typically the dedicated authentication key is used instead.

  2. A decryption (aka E or ENC or DEC) key, used to encrypt and decrypt messages. This technically is not used to encrypt or decrypt any messages directly, but rather derive a unique symmetric-encryption key for each message that can be used to encrypt and decrypt the message.

  3. An authentication (aka A or AUT) key, used for online authentication. This typically is used for signature-based authentication, such as SSH authentication.

The OX agent will use the decryption key of the OpenPGP card as the WireGuard private key. Therefore, the decryption key must be an X25519 key.

Note

X25519 support is a relatively new feature for OpenPGP, so older cards or software may not support it. In particular, YubiKey did not support X25519 until its firmware update 5.2.3, and GnuPG did not support it until version 2.1.

If you use the gpg --list-keys command to view a card’s keys, the decryption key ([E]) must be listed with the cv25519 prefix:

$ gpg --list-keys
pub   ed25519/0xABCDEF1234567890 2023-01-01 [SC]
      1234567890ABCDEF1234567890ABCDEF12345678
uid                   [ultimate] Justin (key1) <justin@example.com>
sub   ed25519/0xFEDCBA0987654321 2023-01-01 [A]
sub   cv25519/0x1234567890ABCDEF 2023-01-01 [E]

If the decryption key is listed with any other prefix, such as rsa2048, rsa3072, or rsa4096, the key cannot be used as a WireGuard key:

$ gpg --list-keys
pub   ed25519/0xABCDEF1234567890 2023-01-01 [SC]
      1234567890ABCDEF1234567890ABCDEF12345678
uid                   [ultimate] Justin (key1) <justin@example.com>
sub   ed25519/0xFEDCBA0987654321 2023-01-01 [A]
sub   rsa3072/0x0987654321FEDCBA 2023-01-01 [E]

However, you can replace the existing decryption key of an OpenPGP card using the gpg-card utility.

Warning

If your replace the existing decryption key of an OpenPGP card, you will not be able to decrypt messages previously encrypted for that key!

To replace the existing decryption key of an OpenPGP card, run the following command:

$ gpg-card generate --algo=cv25519 OPENPGP.2

gpg-card: Note: Keys are already stored on the card!

Replace existing keys? (y/N) y

You will be prompted for the card’s admin PIN; if you enter the admin PIN correctly, a new X25519 key will be generated and overwrite your existing decryption key on the card.

You can immediately use the new decryption key as a WireGuard key; however, it won’t visible on your GnuPG keyring until your edit your keyring’s record for the card’s master key. You must use the gpg --edit-key command to add the new decryption key as a subkey of the card’s master key (and delete the old, replaced decryption key):

$ gpg --edit-key 1234567890ABCDEF1234567890ABCDEF12345678
gpg> addkey
Secret parts of primary key are stored on-card.
Please select what kind of key you want:
...
  (14) Existing key from card
Your selection? 14
Serial number of the card: D2760001240100000006123456780000
Available keys:
   (1) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA OPENPGP.1 ed25519 (cert,sign*)
   (2) DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD OPENPGP.2 rsa3072 (encr*)
   (3) CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC OPENPGP.3 ed25519 (sign,auth*)
Your selection? 2
Please specify how long the key should be valid.
         0 = key does not expire
...
Key is valid for? (0)
Key does not expire at all
Is this correct? (y/N) y
Really create? (y/N) y

sec  ed25519/0xABCDEF1234567890
     created: 2023-01-01  expires: never       usage: SC
     card-no: 0006 12345678
     trust: ultimate      validity: ultimate
ssb  ed25519/0xFEDCBA0987654321
     created: 2023-01-01  expires: never       usage: A
     card-no: 0006 12345678
ssb  rsa3072/0x0987654321FEDCBA
     created: 2023-01-01  expires: never       usage: E
     card-no: 0006 12345678
ssb  cv25519/0x1234567890ABCDEF
     created: 2023-03-10  expires: never       usage: E
     card-no: 0006 12345678
[ultimate] (1). Justin (key1) <justin@example.com>

gpg> key 2

sec  ed25519/0xABCDEF1234567890
     created: 2023-01-01  expires: never       usage: SC
     card-no: 0006 12345678
     trust: ultimate      validity: ultimate
ssb  ed25519/0xFEDCBA0987654321
     created: 2023-01-01  expires: never       usage: A
     card-no: 0006 12345678
ssb* rsa3072/0x0987654321FEDCBA
     created: 2023-01-01  expires: never       usage: E
     card-no: 0006 12345678
ssb  cv25519/0x1234567890ABCDEF
     created: 2023-03-10  expires: never       usage: E
     card-no: 0006 12345678
[ultimate] (1). Justin (key1) <justin@example.com>

gpg> delkey
Do you really want to delete this key? (y/N) y

sec  ed25519/0xABCDEF1234567890
     created: 2023-01-01  expires: never       usage: SC
     card-no: 0006 12345678
     trust: ultimate      validity: ultimate
ssb  ed25519/0xFEDCBA0987654321
     created: 2023-01-01  expires: never       usage: A
     card-no: 0006 12345678
ssb  cv25519/0x1234567890ABCDEF
     created: 2023-03-10  expires: never       usage: E
     card-no: 0006 12345678
[ultimate] (1). Justin (key1) <justin@example.com>

gpg> save
$ gpg --list-keys
pub   ed25519/0xABCDEF1234567890 2023-01-01 [SC]
      1234567890ABCDEF1234567890ABCDEF12345678
uid                   [ultimate] Justin (key1) <justin@example.com>
sub   ed25519/0xFEDCBA0987654321 2023-01-01 [A]
sub   cv25519/0x1234567890ABCDEF 2023-03-10 [E]

If you generate a new decryption key, make sure you distribute your updated key to anyone who may use it to send you encrypted messages (eg via the gpg --export or gpg --send-keys commands).

Host Software

The OWG client uses a UNIX domain socket to communicate with the OX agent, so it will only work on platforms where these kind of sockets are available (and it’s only been tested on Linux).

The OX agent is written in Python, so a moderately recent version of the Python runtime (3.8 or newer) must be available on the host. It also uses the pcsc-lite daemon to communicate with OpenPGP cards, so this must be installed and running on the host as well.

Older versions of GnuPG don’t work well with the pcsc-lite daemon (see the GnuPG and PC/SC Conflicts article). This sometimes can be resolved by adding a disable-ccid flag to your ~/.gnupg/scdaemon.conf config file. However, this doesn’t solve the problem for later versions of the GnuPG 2.2.x branch.

Fortunately, the current GnuPG stable branch (2.4.x) works much better in this regard. You can upgrade to it by downloading the latest GnuPG source code and building it on your local machine. See the Installing GnuPG 2.4 on Ubuntu 22.04 guide for step-by-step instructions.

Installing the Agent

Agent Prerequisites

As the OX agent uses the pcsc-lite daemon, make sure you have it and the pcsc-lite header files installed on your host (you probably already have them if you’ve installed the YubiKey Manager).

On a Debian-based Linux distro (like Ubuntu and friends), you’ll need to install the following packages:

$ sudo apt install libpcsclite-dev pcscd swig
Reading package lists... Done
...
After this operation, 6073 kB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://us.archive.ubuntu.com/ubuntu-ports jammy/main arm64 libpcsclite1 arm64 1.9.5-3 [20.0 kB]
...
Processing triggers for man-db (2.10.2-1) ...

On a Fedora-based distro (like RHEL and friends), you’ll need these packages:

$ sudo dnf install pcsc-lite pcsc-lite-devel swig
Last metadata expiration check: ...
...
Installed size: 5.9 M
Is this ok [y/N]: y
Downloading Packages:
...
Complete!

Once you’ve installed the pcsc-lite daemon, make sure it’s running:

$ systemctl status pcscd
○ pcscd.service - PC/SC Smart Card Daemon
     Loaded: loaded (/usr/lib/systemd/system/pcscd.service; indirect; preset: disabled)
     Active: inactive (dead)
TriggeredBy: ● pcscd.socket
       Docs: man:pcscd(8)
$ sudo systemctl start pcscd
$ systemctl status pcscd
● pcscd.service - PC/SC Smart Card Daemon
     Loaded: loaded (/usr/lib/systemd/system/pcscd.service; indirect; preset: disabled)
     Active: active (running) since Thu 2023-03-09 00:29:54 UTC; 1s ago
TriggeredBy: ● pcscd.socket
       Docs: man:pcscd(8)
   Main PID: 2479 (pcscd)
      Tasks: 3 (limit: 966)
     Memory: 1.3M
        CPU: 8ms
     CGroup: /system.slice/pcscd.service
             └─2479 /usr/sbin/pcscd --foreground --auto-exit

Mar 09 00:29:54 colossus systemd[1]: Started pcscd.service - PC/SC Smart Card Daemon.

Also, make sure you have Python 3.8 or newer installed on the host, with the Python header files (and some basic build tools like Make and GCC).

On a Debian-based Linux distro (like Ubuntu and friends), you’ll need the following packages:

$ sudo apt install gcc make python3-dev python3-venv
Reading package lists... Done
...
After this operation, 168 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://us.archive.ubuntu.com/ubuntu-ports jammy-updates/main arm64 gcc-11-base arm64 11.3.0-1ubuntu1~22.04 [20.8 kB]
...
Processing triggers for man-db (2.10.2-1) ...

On a Fedora-based distro (like RHEL and friends), you’ll need these packages:

$ sudo dnf install findutils gcc make python3-devel
Last metadata expiration check: ...
...
Installed size: 189 M
Is this ok [y/N]: y
Downloading Packages:
...
Complete!

Agent Virtual Environment

The OX agent is a Python package. The best way to install it is in its own Python virtual environment (venv) — this makes it easy to uninstall, and also avoids conflicts with other Python tools you may have installed on the same host.

Run the following commands to create a new venv directory for the OX agent, change its owner to your daily user account, and initialize the venv:

$ sudo mkdir -p /opt/venvs/openpgpcard-x25519-agent
$ sudo chmod o+rx /opt/venvs /opt/venvs/openpgpcard-x25519-agent
$ sudo chown $(id -u):$(id -g) /opt/venvs/openpgpcard-x25519-agent
$ umask 0002
$ python3 -m venv --upgrade-deps /opt/venvs/openpgpcard-x25519-agent
$ ls -ld /opt/venvs/openpgpcard-x25519-agent
drwxr-xr-x. 1 justin justin 56 Mar  9 00:38 /opt/venvs/openpgpcard-x25519-agent

Agent Python Package

Next, install the latest version of the OX agent package from PyPi into the venv:

$ . /opt/venvs/openpgpcard-x25519-agent/bin/activate
(openpgpcard-x25519-agent) $ pip install openpgpcard-x25519-agent
Collecting openpgpcard-x25519-agent
...
Successfully installed OpenPGPpy-1.1 docopt-ng-0.8.1 openpgpcard-x25519-agent-1.0.0 pyscard-2.0.3
(openpgpcard-x25519-agent) $ deactivate
$ ls -l /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard*
-rwxrwxr-x. 1 justin justin 258 Mar  9 00:43 /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-agent
-rwxrwxr-x. 1 justin justin 265 Mar  9 00:43 /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client

You should now be able to run the OX agent:

$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-agent --help
OpenPGP Card X25519 Agent.

Socket interface to Curve25519 ECDH from an OpenPGP card.

Usage:
  openpgpcard-x25519-agent [--card=ID] [-v | -vv | --verbosity=LEVEL]
  openpgpcard-x25519-agent --listen [--socket=SOCKET] [--card=ID]
                           [-v | -vv | --verbosity=LEVEL]
  openpgpcard-x25519-agent --test [--card=ID] [-v | -vv | --verbosity=LEVEL]
  openpgpcard-x25519-agent --help
  openpgpcard-x25519-agent --version

Options:
  -h --help             Show this help
  --version             Show agent version
  -c --card=ID          Card to use (default: first found)
  -l --listen           Listen on socket
  -s --socket=SOCKET    Socket path (default: /var/run/wireguard/agent0)
  --test                Prompt for PIN and attempt a test X25519 operation.
  --verbosity=LEVEL     Log level (ERROR, WARNING, INFO, DEBUG)
  -v                    INFO verbosity
  -vv                   DEBUG verbosity

And if you have an OpenPGP card plugged into the host, you should be able to check its details:

$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-agent
Card Index: 0x0
Card Name: Yubico YubiKey OTP+FIDO+CCID 00 00
Serial Number: 0x12345678
Manufacturer: 0x0006 (Yubico)
OpenPGP Version: 3.4
Signature Key: ed25519
Encryption Key: x25519 (/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=)
Authentication Key: ed25519
PIN Status: 3 tries remaining
----------
Card Index: 0x1
Card Name: Yubico YubiKey OTP+FIDO+CCID 00 00
Serial Number: 0x23456789
Manufacturer: 0x0006 (Yubico)
OpenPGP Version: 3.4
Signature Key: ed25519
Encryption Key: x25519 (hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k=)
Authentication Key: ed25519
PIN Status: 3 tries remaining

If you have multiple OpenPGP cards plugged in, you’ll see multiple cards listed. You can use the serial number of a card (in hex) to identify it uniquely:

$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-agent --card=0x12345678
Card Index: 0x0
Card Name: Yubico YubiKey OTP+FIDO+CCID 00 00
Serial Number: 0x12345678
Manufacturer: 0x0006 (Yubico)
OpenPGP Version: 3.4
Signature Key: ed25519
Encryption Key: x25519 (/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=)
Authentication Key: ed25519
PIN Status: 3 tries remaining

As noted in the Requirements and Limitations section, to use the decryption key of an OpenPGP card as a WireGuard key, it must be an X25519 key. If a card’s Encryption Key from the output above is not listed as x25519, it cannot be used as a WireGuard key.

If the decryption key can be used as a WireGuard key, the public key will be shown in base64 format on the Encryption Key line, ready to be copied and pasted into the WireGuard config files of the host’s WireGuard peers (in the above example output, this public key is /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=).

Agent Socket Group

The OX agent will listen as an SSH agent on a UNIX domain socket, and provide X25519 shared secrets from the OpenPGP card to any clients that can access this socket. For security, we’ll limit access to the socket to the members of a single group. Create a new openpgpcard group for this purpose with the following command:

$ sudo groupadd --system openpgpcard

For maximum convenience, add your daily user account to the group:

$ sudo usermod --append --groups openpgpcard $USER
$ grep openpgpcard /etc/group
openpgpcard:x:993:justin

Alternatively, for maximum security, don’t add yourself (or any other user) to this group. Instead, whenever you want to set or clear the cached PIN on the OX agent (which requires access to the socket on which the agent is listening), use the sudo command with the openpgpcard group to do so:

$ sudo -g openpgpcard ...
Note

If you add yourself to the openpgpcard group, will need to log out of your current desktop session and log back in again in order to use the new group. Rather than bothering with that now, just prefix any openpgpcard-x25519-client commands shown in the rest of this article with sudo -g openpgpcard until you reach a convenient point to log out and log back in.

Agent Systemd Daemon

Finally, start the OX agent listening on its default /var/run/wireguard/agent0 socket.

To set it up as a systemd service, copy the following systemd service unit content to /usr/local/lib/systemd/system/openpgpcard-x25519-agent.service:

# /usr/local/lib/systemd/system/openpgpcard-x25519-agent.service
[Unit]
Description=OpenPGP Card X25519 Agent
After=network-online.target nss-lookup.target
Requires=openpgpcard-x25519-agent.socket

[Service]
Type=simple
ExecStart=/opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-agent --listen --verbosity=INFO

# optionally copy the much-longer set of HARDENING settings from here (recommended):
# https://git.sr.ht/~arx10/openpgpcard-x25519-agent/tree/main/item/etc/openpgpcard-x25519-agent.service
DynamicUser=yes
RestrictAddressFamilies=AF_UNIX

[Install]
WantedBy=default.target

If you installed the OX agent to a venv other than /opt/venvs/openpgpcard-x25519-agent, make sure you adjust the ExecStart setting above to use the correct directory. If you have multiple OpenPGP cards available that you may use with the same host, adjust the ExecStart command to select the serial number of the card the agent should use:

ExecStart=/opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-agent --listen --card=0x12345678 --verbosity=INFO

Copy the following systemd socket unit content to /usr/local/lib/systemd/system/openpgpcard-x25519-agent.socket:

# /usr/local/lib/systemd/system/openpgpcard-x25519-agent.socket
[Unit]
Description=OpenPGP Card X25519 Agent Socket
PartOf=openpgpcard-x25519-agent.service

[Socket]
ListenStream=/var/run/wireguard/agent0
RemoveOnStop=yes

# HARDENING
SocketMode=0660
SocketGroup=openpgpcard

[Install]
WantedBy=sockets.target

If you named the socket group something other than openpgpcard in the preceding Agent Socket Group step, make sure you adjust the SocketGroup setting above to match it. The OWG client is hardcoded to connect to an /var/run/wireguard/agentX socket, so don’t change this path (other than to change the number at the end).

Then load the service settings and start the service by running the following commands:

$ sudo systemctl daemon-reload
$ sudo systemctl --now enable openpgpcard-x25519-agent
Created symlink /etc/systemd/system/default.target.wants/openpgpcard-x25519-agent.service → /usr/local/lib/systemd/system/openpgpcard-x25519-agent.service.
$ systemctl status openpgpcard-x25519-agent
● openpgpcard-x25519-agent.service - OpenPGP Card X25519 Agent
     Loaded: loaded (/usr/local/lib/systemd/system/openpgpcard-x25519-agent.service; enabled; preset: disabled)
     Active: active (running) since Thu 2023-03-09 03:28:45 UTC; 2s ago
TriggeredBy: ● openpgpcard-x25519-agent.socket
   Main PID: 159488 (openpgpcard-x25)
      Tasks: 1 (limit: 966)
     Memory: 13.2M
        CPU: 130ms
     CGroup: /system.slice/openpgpcard-x25519-agent.service
             └─159488 /opt/venvs/openpgpcard-x25519-agent/bin/python3 /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-agent --listen --verbosity=INFO

Mar 09 03:28:45 colossus systemd[1]: Started openpgpcard-x25519-agent.service - OpenPGP Card X25519 Agent.
Mar 09 03:28:46 colossus openpgpcard-x25519-agent[159488]: 2023-03-09 03:28:46,083 openpgpcard_x25519_agent.server.Server INFO: binding to 3

Alternatively, if you don’t want to set up a daemon for the OX agent just yet, you can run it manually. To run it as an unprivileged user (like your daily user account), make sure you first create a /var/run/wireguard directory in which the agent can create a socket file on which to listen, and then run the agent with the --listen flag:

$ sudo mkdir -p /var/run/wireguard
$ sudo chown $USER /var/run/wireguard
$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-agent --listen

With the OX agent up and running, you can use the openpgcard-x25519-client command (the OX client) to interact with the agent. Test it out by running the following command:

$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --public-key
/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
Note

Prefix this command with sudo -g openpgpcard if you have not added your daily user account to the openpgpcard group, or if you have not logged out and in again since you added your account to that group:

$ sudo -g openpgpcard /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --public-key
/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=

It should print out the WireGuard public key of your OpenPGP card (/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU= in this example).

Installing the Client

Once you have the OX agent working, you can install the OWG client.

Client Prerequisites

You will need the standard wg utility to use the OWG client (and on Linux, you’ll also need the iproute2 tool suite if you’re trying to install the client in a minimal VM or container that doesn’t already have the iproute2 tools installed).

On a Debian-based Linux distro (like Ubuntu and friends), install the following packages:

$ sudo apt install iproute2 wireguard-tools
Reading package lists... Done
...
After this operation, 320 kB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://us.archive.ubuntu.com/ubuntu-ports jammy/main arm64 wireguard-tools arm64 1.0.20210914-1ubuntu2 [20.1 kB]
...
Processing triggers for man-db (2.10.2-1) ...

On a Fedora-based distro (like RHEL and friends), you’ll need the these packages:

$ sudo dnf install iproute wireguard-tools
Last metadata expiration check: ...
...
Installed size: 326 k
Is this ok [y/N]: y
Downloading Packages:
...
Complete!

The wg utility must be version v1.0.20210223 or newer:

$ wg version
wireguard-tools v1.0.20210223 - https://git.zx2c4.com/wireguard-tools/

If your package manager installs an older version, you’ll need to uninstall it, and build and install a newer version from the source wireguard-tools project:

$ git clone https://git.zx2c4.com/wireguard-tools
Cloning into 'wireguard-tools'...
...
Resolving deltas: 100% (2088/2088), done.
$ make -C wireguard-tools/src -j$(nproc)
make: Entering directory `/home/justin/wireguard-tools/src'
  CC      wg.o
  ...
  LD      wg
make: Leaving directory `/home/justin/wireguard-tools/src'
$ sudo make -C wireguard-tools/src install
make: Entering directory `/home/justin/wireguard-tools/src'
‘wg’ -> ‘/usr/bin/wg’
...
‘systemd/wg-quick.target’ -> ‘/usr/lib/systemd/system/wg-quick.target’
make: Leaving directory `/home/justin/wireguard-tools/src'
$ wg version
wireguard-tools v1.0.20210914 - https://git.zx2c4.com/wireguard-tools/

Client Go Binary

You can download the x86_64 or aarch64 binaries for the client from OWG builds page on SourceHut (click the build number of the latest successful build, then click the download link for the appropriate binary in the “Artifacts” section of the build details).

Alternatively, you can clone the OWG source repository, and build it yourself:

$ git clone https://git.sr.ht/~arx10/openpgpcard-wireguard-go
Cloning into 'openpgpcard-wireguard-go'...
...
Resolving deltas: 100% (4552/4552), done.
$ cd openpgpcard-wireguard-go
$ make
go build -v -o "wireguard-go"
...
golang.zx2c4.com/wireguard
$ ls -l wireguard-go
-rwxr-xr-x. 1 justin justin 3719736 Mar  9 03:47 wireguard-go

Note that building from source requires having a build environment for Go version 1.19 or newer.

Copy the binary to the /usr/local/bin/openpgpcard-wireguard-go directory, and make sure it’s executable:

$ chmod 755 wireguard-go
$ sudo cp wireguard-go /usr/local/bin/openpgpcard-wireguard-go

Custom Wg-Quick Script

To use a convenient wg-quick-like executable with the OWG client, copy and run the owg-quick-maker.sh script from the OWG source repo. If you didn’t build the OWG client from source, download this script, make it executable, and then run it:

$ wget https://git.sr.ht/~arx10/openpgpcard-wireguard-go/blob/wg20230223/owg-quick-maker.sh
Resolving git.sr.ht (git.sr.ht)... 173.195.146.142
...
2023-03-09 03:45:24 (77.3 MB/s) - ‘owg-quick-maker.sh’ saved [1814/1814]
$ chmod +x owg-quick-maker.sh
$ sudo ./owg-quick-maker.sh
-rwxr-xr-x. 1 root root 13536 Mar  9 03:56 /usr/local/bin/owg-quick
-rw-r--r--. 1 root root 853 Mar  9 03:56 /usr/local/lib/systemd/system/owg-quick@.service

This script will copy the original wg-quick executable you installed in the Client Prerequisites step as a new executable at /usr/local/bin/owg-quick; this new executable will always run /usr/local/bin/openpgpcard-wireguard-go when starting up a WireGuard interface, instead of using the native WireGuard kernel module. If you’re using systemd, it will also copy the WireGuard systemd service template to /usr/local/lib/systemd/system/owg-quick@.service, and make a similar adjustment to use the owg-quick executable instead of wg-quick.

Usage

Remote WireGuard Configuration

With the OX agent listening on the /var/run/wireguard/agent0 socket, run the following command to retrieve the WireGuard public key from the OpenPGP card:

$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --public-key
/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
Note

Prefix this command with sudo -g openpgpcard if you have not added your daily user account to the openpgpcard group, or if you have not logged out and in again since you added your account to that group:

$ sudo -g openpgpcard /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --public-key
/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=

Copy this public key to the host’s remote WireGuard peers, and use it in their configuration to connect to the host. For example, if you’re using the OpenPGP card on Endpoint A of the example scenario from the WireGuard Point to Point Configuration guide, you would configure the other endpoint, Endpoint B, like this:

# /etc/wireguard/wg0.conf

# local settings for Endpoint B
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51822

# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32

Local WireGuard Configuration

In your local WireGuard configuration, set the interface’s PrivateKey value to AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=:

# /etc/wireguard/wg0.conf

# local settings for Endpoint A
[Interface]
# openpgpcard-x25519-agent socket 0 seat 0
PrivateKey = AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
Address = 10.0.0.1/32
ListenPort = 51821

# remote settings for Endpoint B
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 203.0.113.2:51822
AllowedIPs = 10.0.0.2/32

This “private key” setting of AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= is a dummy value (and not a real X25519 private key) which signals to the OWG client that it should use the OX agent socket at /var/run/wireguard/agent0 to run private key operations for the interface.

The key, in fact, consists simply of a 1 for the first byte, followed by 31 zeros, as you can see if you run the following command (which shows the key as hex):

$ echo AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= | base64 -d | xxd -p -c32
0100000000000000000000000000000000000000000000000000000000000000

If you want the client to use a different socket (like because you want to use multiple different keys for multiple different WireGuard interfaces), you can specify a different socket by adjusting the second byte in the dummy key. For example, to use the socket at /var/run/wireguard/agent2, set the second byte to 2:

$ echo 0102000000000000000000000000000000000000000000000000000000000000 | xxd -p -r | base64
AQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

Set Card PIN

Run the following OX client command to cache the PIN for OpenPGP card with the OX agent:

$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --set-pin --prompt
PIN:
Note

Prefix this command with sudo -g openpgpcard if you have not added your daily user account to the openpgpcard group, or if you have not logged out and in again since you added your account to that group:

$ sudo -g openpgpcard /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --set-pin --prompt
PIN:

The OX agent needs the PIN in order to use the card’s decryption key to perform WireGuard handshakes. You will not be able to use the WireGuard interface without providing the PIN to the agent.

Rather that typing in the PIN manually, you can have it read in from stdin like the following:

$ pass smartcards/card1/pin | /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --set-pin

Or read from a file (or file descriptor), like the following:

$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --set-pin <(echo 123456)

You can clear the PIN from the OX agent’s cache by running the following command:

$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --clear-pin

This will prevent the WireGuard interface from being used for new connections; and for any active WireGuard connections, they will become unusable within 3 minutes (depending on the time of their last handshake).

Run Owg-Quick Script

With the OX agent running and the PIN set, start the OWG client by running the owg-quick script created in the Custom Wg-Quick Script step above:

$ sudo owg-quick up wg0
[#] /usr/local/bin/openpgpcard-wireguard-go wg0
┌──────────────────────────────────────────────────────┐
│                                                      │
│   Running wireguard-go is not required because this  │
│   kernel has first class support for WireGuard. For  │
│   information on installing the kernel module,       │
│   please visit:                                      │
│         https://www.wireguard.com/install/           │
│                                                      │
└──────────────────────────────────────────────────────┘
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.0.1/32 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] ip -4 route add 10.0.0.2/32 dev wg0

If you’re using a YubiKey with touch required for the OpenPGP decryption key, the YubiKey will start blinking. If your touch setting is On (or Fixed), you will have to touch it once for each peer configured for the WireGuard interface. If your touch setting is Cached (or Cached-Fixed), you’ll have to touch it just once (regardless of the number of peers). You need to perform these touches within 15 seconds of the interface starting up (or the YubiKey will time-out the interaction).

If you don’t perform all the required touches when the interface starts up, the interface won’t be able to perform the shared-secret pre-computations needed for each peer — and as a result, the connections for the peers missing these pre-computations will not work. So if you fail to perform all the necessary touches within the time limit on start up, shut the interface down (sudo owg-quick down wg0), and then start it up again (sudo owg-quick up wg0).

If you use systemd, you can also use the owg-quick@.service template unit just like you use wg-quick@.service to start or stop an interface:

$ sudo systemctl start owg-quick@wg0
$ systemctl status owg-quick@wg0
● owg-quick@wg0.service - WireGuard via openpgpcard-wireguard-go for wg0
     Loaded: loaded (/usr/local/lib/systemd/system/owg-quick@.service; disabled; vendor preset: enabled)
     Active: active (exited) since Thu 2023-03-09 03:54:43 UTC; 2s ago
       Docs: https://git.sr.ht/~arx10/openpgpcard-wireguard-go
             ...
    Process: 791283 ExecStart=/usr/local/bin/owg-quick up wg0 (code=exited, status=0/SUCCESS)
   Main PID: 791283 (code=exited, status=0/SUCCESS)
      Tasks: 15 (limit: 18945)
     Memory: 7.2M
        CPU: 3.447s
     CGroup: /system.slice/system-owg\x2dquick.slice/owg-quick@wg0.service
             └─791299 /usr/local/bin/openpgpcard-wireguard-go wg0

Mar 09 03:54:43 colossus owg-quick[310691]: │   information on installing the kernel module,       │
Mar 09 03:54:43 colossus owg-quick[310691]: │   please visit:                                      │
Mar 09 03:54:43 colossus owg-quick[791292]: │         https://www.wireguard.com/install/           │
Mar 09 03:54:43 colossus owg-quick[791292]: │                                                      │
Mar 09 03:54:43 colossus owg-quick[791292]: └──────────────────────────────────────────────────────┘
Mar 09 03:54:43 colossus owg-quick[791283]: [#] wg setconf wg0 /dev/fd/63
Mar 09 03:54:43 colossus owg-quick[791283]: [#] ip -4 address add 10.0.0.1/32 dev wg0
Mar 09 03:54:43 colossus owg-quick[791283]: [#] ip link set mtu 1420 up dev wg0
Mar 09 03:54:43 colossus owg-quick[791283]: [#] ip -4 route add 10.0.0.2/32 dev wg0
Mar 09 03:54:43 colossus systemd[1]: Finished WireGuard via openpgpcard-wireguard-go for wg0.
Caution

You can use the wg command with the OWG client as you do normally with WireGuard — but if you run the wg command to show the status of a WireGuard interface set up with the OWG client, it will show a dummy public key value for the interface (matching the dummy private key used in the interface’s config file) — not the real public key value:

$ wg-quick show wg0
interface: wg0
  public key: L+V9o0fNYkMVKNqsX7spBzD/9oSvxM/C7ZCZX1jLO3Q=
  private key: (hidden)
  listening port: 51821

peer: fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
  endpoint: 203.0.113.2:51822
  allowed ips: 10.0.0.2/32
  latest handshake: 5 seconds ago
  transfer: 2.38 KiB received, 3.46 KiB sent

To see the real public key for the interface, run the openpgpcard-x25519-agent command:

$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-agent
Card Index: 0x0
Card Name: Yubico YubiKey OTP+FIDO+CCID 00 00
Serial Number: 0x12345678
Manufacturer: 0x0006 (Yubico)
OpenPGP Version: 3.4
Signature Key: ed25519
Encryption Key: x25519 (/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=)
Authentication Key: ed25519
PIN Status: 3 tries remaining

Or run the openpgpcard-x25519-client --public-key command (when the OX agent is listening on its socket):

$ /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --public-key
/TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=

The real public key from the openpgpcard-x25519-agent or openpgpcard-x25519-client command is what you must use when configuring the remote peers of the interface.

Use the WireGuard Interface

You can now start using the WireGuard interface as normal. With our example scenario from the WireGuard Point to Point Configuration guide, you should be able to connect to a webserver running on Endpoint B from Endpoint A:

$ curl 10.0.0.2
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...

However, if you’re using a YubiKey with touch required for the OpenPGP decryption key, as soon as you start trying to use the connection, the YubiKey will start blinking again. You must touch the YubiKey to start using the connection. While you’re using the connection, the YubiKey will resume blinking every 2 minutes, and you must touch the YubiKey within a minute of its resumed blinking, or the connection will stop working.

Furthermore, if your touch setting is On (or Fixed), you’ll have to touch the YubiKey within 5 seconds of its resumed blinking, or touch requests will start to queue up, and you’ll have to touch it multiple times to clear the queue.

Tip

If you have the YubiKey Manager CLI installed, and the touch setting of your OpenPGP decryption key is not set to Fixed or Cached-Fixed, you can turn off the touch requirement by running the following command:

$ ykman openpgp keys set-touch enc off
Enter Admin PIN:
Set touch policy of ENC key to off? [y/N]: y

Or change the touch requirement to cached (for 15 seconds after a touch) with the following command:

$ ykman openpgp keys set-touch enc cached
Enter Admin PIN:
Set touch policy of ENC key to cached? [y/N]: y

Usage Recap

The typical daily usage of the OX agent and OWG client would look something like this:

  1. Start the OX agent if not already running: sudo systemctl start openpgpcard-x25519-agent

  2. Provide the agent with the OpenPGP card PIN: /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --set-pin --prompt

  3. Plug in the OpenPGP card if not already plugged in

  4. Start up the OWG client: sudo systemctl start owg-quick@wg0

  5. Use the WireGuard connection

  6. Shut down the OWG client: sudo systemctl stop owg-quick@wg0

  7. Clear the PIN from the agent: /opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-client --clear-pin

  8. Unplug the OpenPGP card

Troubleshooting

PCSC Daemon Output

The first step to troubleshooting is to make sure the pcsc-lite daemon is running, and to check its logs:

$ systemctl status pcscd
● pcscd.service - PC/SC Smart Card Daemon
     Loaded: loaded (/usr/lib/systemd/system/pcscd.service; indirect; preset: disabled)
     Active: active (running) since Thu 2023-03-09 00:29:54 UTC; 1s ago
TriggeredBy: ● pcscd.socket
       Docs: man:pcscd(8)
   Main PID: 2479 (pcscd)
      Tasks: 3 (limit: 966)
     Memory: 1.3M
        CPU: 8ms
     CGroup: /system.slice/pcscd.service
             └─2479 /usr/sbin/pcscd --foreground --auto-exit

Mar 09 00:29:54 colossus systemd[1]: Started pcscd.service - PC/SC Smart Card Daemon.
$ journalctl -u pcscd
Mar 09 00:29:54 colossus systemd[1]: Started pcscd.service - PC/SC Smart Card Daemon.

If it’s not running, the OX agent won’t work (which means the OWG client won’t work, which means your WireGuard connection won’t work). If it is running, but you see errors in the pcsc-daemon logs, they may point to the root cause of the problem.

In particular, if you see errors like the following in the pcsc-daemon logs, you probably need to resolve its conflict with GnuPG’s scdaemon:

Mar 08 10:48:06 colossus pcscd[916015]: 00000000 ccid_usb.c:672:OpenUSBByName() Can't claim interface 1/7: LIBUSB_ERROR_BUSY

Or:

Mar 08 11:08:34 colossus pcscd[552957]: 99999999 winscard.c:286:SCardConnect() Error Reader Exclusive

If the suggestion from the GnuPG and PC/SC Conflicts article doesn’t help, try installing the latest stable version of GnuPG (see the Installing GnuPG 2.4 on Ubuntu 22.04 article for detailed instructions).

Agent Output

If the pcsc-lite daemon is happy, check the OX agent’s logs. If running as a systemd service, you can page through the agent’s full logs by running the following command:

$ journalctl -u openpgpcard-x25519-agent
Mar 09 03:28:45 colossus systemd[1]: Started openpgpcard-x25519-agent.service - OpenPGP Card X25519 Agent.
Mar 09 03:28:46 colossus openpgpcard-x25519-agent[159488]: 2023-03-09 03:28:46,083 openpgpcard_x25519_agent.server.Server INFO: binding to 3
Tip

Add the -f flag (for --follow) to the journalctl command to “tail” the logs:

$ journalctl -u openpgpcard-x25519-agent -f

At the INFO level of verbosity, the OX agent logs will include two log entries for each client request it receives: one message noting the type of request, and one noting its response:

Mar 09 11:55:18 colossus openpgpcard-x25519-agent[328963]: 2023-03-09 11:55:18,295 openpgpcard_x25519_agent.msg.Message INFO: parsing request for public key
Mar 09 11:55:18 colossus openpgpcard-x25519-agent[328963]: 2023-03-09 11:55:18,314 openpgpcard_x25519_agent.msg.Message INFO: formatting response with public key

If something goes wrong handling the request, the logs will show the stack trace of the error, and at the bottom of the stack trace, the root cause:

Mar 09 12:07.48 colossus openpgpcard-x25519-agent[284142]: 2023-03-09 12:07:48,851 openpgpcard_x25519_agent.msg.Message INFO: parsing request to derive shared secret
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]: 2023-03-09 12:08:03,329 openpgpcard_x25519_agent.server.Server ERROR: error handling request
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]: Traceback (most recent call last):
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:   File "/opt/venvs/openpgpcard-x25519-agent/lib/python3.10/site-packages/openpgpcard_x25519_agent/card.py", line 298, in calculate_shared_secret
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:     return calculate_x25519_shared_secret(card, public_key)
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:   File "/opt/venvs/openpgpcard-x25519-agent/lib/python3.10/site-packages/openpgpcard_x25519_agent/card.py", line 273, in calculate_x25519_shared_secret
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:     return send_simple_command(card, 0, 0x2A, 0x80, 0x86, data)
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:   File "/opt/venvs/openpgpcard-x25519-agent/lib/python3.10/site-packages/openpgpcard_x25519_agent/card.py", line 207, in send_simple_command
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:     raise PGPCardException(status_1, status_2)
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]: OpenPGPpy.openpgp_card.PGPCardException: Error status : 0x6982
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]: During handling of the above exception, another exception occurred:
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]: Traceback (most recent call last):
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:   File "/opt/venvs/openpgpcard-x25519-agent/lib/python3.10/site-packages/openpgpcard_x25519_agent/server.py", line 129, in handle_connection
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:     self.handle_request(request, response)
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:   File "/opt/venvs/openpgpcard-x25519-agent/lib/python3.10/site-packages/openpgpcard_x25519_agent/server.py", line 158, in handle_request
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:     self.request_extension(request, response)
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:   File "/opt/venvs/openpgpcard-x25519-agent/lib/python3.10/site-packages/openpgpcard_x25519_agent/server.py", line 176, in request_extension
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:     self.derive_shared_secret(request, response)
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:   File "/opt/venvs/openpgpcard-x25519-agent/lib/python3.10/site-packages/openpgpcard_x25519_agent/server.py", line 198, in derive_shared_secret
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:     response.shared_secret = calculate_shared_secret(
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:   File "/opt/venvs/openpgpcard-x25519-agent/lib/python3.10/site-packages/openpgpcard_x25519_agent/card.py", line 303, in calculate_shared_secret
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:     return calculate_x25519_shared_secret(card, public_key)
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:   File "/opt/venvs/openpgpcard-x25519-agent/lib/python3.10/site-packages/openpgpcard_x25519_agent/card.py", line 273, in calculate_x25519_shared_secret
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:     return send_simple_command(card, 0, 0x2A, 0x80, 0x86, data)
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:   File "/opt/venvs/openpgpcard-x25519-agent/lib/python3.10/site-packages/openpgpcard_x25519_agent/card.py", line 207, in send_simple_command
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]:     raise PGPCardException(status_1, status_2)
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]: OpenPGPpy.openpgp_card.PGPCardException: Error status : 0x6600
Mar 09 12:08:03 colossus openpgpcard-x25519-agent[284142]: 2023-03-09 12:08:03,332 openpgpcard_x25519_agent.msg.Message INFO: formatting response with failure

The above error is what you’d see with a YubiKey with touch required, when you don’t touch it within 15 seconds of it starting to blink.

Agent Debug Output

You can get a little more detail about specific messages sent back and forth between the OX agent and the OpenPGP card by cranking the agent’s log level up to DEBUG.

If the agent is running as a systemd service, edit its service unit file to change its --verbosity flag to DEBUG:

# /usr/local/lib/systemd/system/openpgpcard-x25519-agent.service
[Unit]
Description=OpenPGP Card X25519 Agent
After=network-online.target nss-lookup.target
Requires=openpgpcard-x25519-agent.socket

[Service]
Type=simple
ExecStart=/opt/venvs/openpgpcard-x25519-agent/bin/openpgpcard-x25519-agent --listen --verbosity=DEBUG

# HARDENING
DevicePolicy=closed
DynamicUser=yes
IPAddressDeny=any
...

Then reload the unit configuration and restart the OX agent:

$ sudo systemctl daemon-reload
$ sudo systemctl restart openpgpcard-x25519-agent

You’ll now see some more low-level details about the interaction between the OX agent and the OpenPGP card in the agent’s log:

$ journalctl -u openpgpcard-x25519-agent -f
Mar 09 13:58:23 colossus systemd[1]: Started openpgpcard-x25519-agent.service - OpenPGP Card X25519 Agent.
Mar 09 13:58:24 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:58:24,406 openpgpcard_x25519_agent.cnf DEBUG: init logging at DEBUG
Mar 09 13:58:24 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:58:24,406 openpgpcard_x25519_agent.server.Server DEBUG: binding interrupt
Mar 09 13:58:24 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:58:24,407 openpgpcard_x25519_agent.server.Server INFO: binding to 3
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,358 openpgpcard_x25519_agent.server.Server DEBUG: new connection
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,359 openpgpcard_x25519_agent.msg.Message DEBUG: parsing request for public key
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,361 OpenPGPpy.openpgp_card DEBUG: Available readers :
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,361 OpenPGPpy.openpgp_card DEBUG:  - Yubico YubiKey OTP+FIDO+CCID 00 00
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,361 OpenPGPpy.openpgp_card DEBUG: Using reader index #0
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,362 OpenPGPpy.openpgp_card DEBUG: Trying with reader : Yubico YubiKey OTP+FIDO+CCID 00 00
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,363 OpenPGPpy.openpgp_card DEBUG:  Sending 0xA4 command with 6 bytes data
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,364 OpenPGPpy.openpgp_card DEBUG: -> 00 A4 04 00 06 D2 76 00 01 24 01
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,366 OpenPGPpy.openpgp_card DEBUG:  Received 0 bytes data : SW 0x9000 - duration: 1.9 ms
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,366 OpenPGPpy.openpgp_card DEBUG: An OpenPGP applet detected, using Yubico YubiKey OTP+FIDO+CCID 00 00
Mar 09 13:59:14 colossus openpgpcard-x25519-agent[179412]: 2023-09-03 13:59:14,366 OpenPGPpy.openpgp_card DEBUG: Read Data  in 0x004F
...

Run Client in Foreground

To view logging from the OWG client, you must run it in the foreground (instead of as a daemon). This means you won’t be able to use the owg-quick helper executable to configure the interface for you — instead you’ll have to configure the interface manually.

First, start up the OWG client in one terminal like the following:

$ sudo LOG_LEVEL=debug openpgpcard-wireguard-go --foreground wg0
┌──────────────────────────────────────────────────────┐
│                                                      │
│   Running wireguard-go is not required because this  │
│   kernel has first class support for WireGuard. For  │
│   information on installing the kernel module,       │
│   please visit:                                      │
│         https://www.wireguard.com/install/           │
│                                                      │
└──────────────────────────────────────────────────────┘
DEBUG: (wg0) 2023/03/09 15:59:05 Starting wireguard-go version wg20230223-owg1
DEBUG: (wg0) 2023/03/09 15:59:05 Device started
DEBUG: (wg0) 2023/03/09 15:59:05 UAPI listener started
DEBUG: (wg0) 2023/03/09 15:59:05 Routine: decryption worker 2 - started
...

Then in a separate terminal, configure the WireGuard interface manually, like with the following commands:

$ echo AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= | sudo wg set wg0 private-key /dev/stdin listen-port 51821
$ sudo wg set wg0 peer fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds= endpoint 203.0.113.2:51822 allowed-ips 10.0.0.2/32
$ sudo ip -4 address add 10.0.0.1/32 dev wg0
$ sudo ip link set mtu 1420 up dev wg0
$ sudo ip -4 route add 10.0.0.2/32 dev wg0

When you set the interface’s private key, you should see the OWG client print the following entries (displaying the OpenPGP card’s real public key, as received from the OX agent):

DEBUG: (wg0) 2023/03/09 17:36:58 UAPI: Updating private key
DEBUG: (wg0) 2023/03/09 17:36:58 Device public key: /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=

And when the OWG client is able to successfully perform a WireGuard handshake using the shared secret derived through the OX agent, you’ll see it print entries like the following:

DEBUG: (wg) 2023/03/09 17:43:21 peer(fE/w…S6Ds) - Sending handshake initiation
DEBUG: (wg) 2023/03/09 17:43:21 peer(fE/w…S6Ds) - Received handshake response

Or:

DEBUG: (wg) 2023/03/09 17:57:29 peer(fE/w…S6Ds) - Received handshake initiation
DEBUG: (wg) 2023/03/09 17:57:29 peer(fE/w…S6Ds) - Sending handshake response

If the OX agent fails to derive a shared secret (like for example, because it hasn’t been set with the OpenPGP card’s PIN yet), you’ll instead see entries like the following:

ERROR: (wg0) 2023/03/09 18:00:01 Failed to derive shared secret from static private key: unexpected card response to x25519/dh: 5

And if the OWG client isn’t able to connect to the OX agent at all, you’ll see entries like the following:

ERROR: (wg0) 2023/03/09 18:00:12 Failed to derive shared secret from static private key: read unix @->/run/wireguard/agent0: i/o timeout

Dangers

The benefit to using WireGuard like this is that your private key is stored safely on an OpenPGP card, and cannot be stolen by a remote adversary. However, there are a number of dangers that the OX agent and OWG client introduces:

Dummy Private Key

If you accidentally use the dummy private key needed for the OWG client with any other WireGuard client, the other WireGuard client may try to use the dummy key as a real WireGuard key. If an adversary intercepts traffic encrypted with a dummy key, she could easily guess the key and decrypt the traffic. In that case, the adversary could also impersonate you and send and receive traffic encrypted using that dummy key on your behalf.

This can be mitigated to a degree by using the default dummy key AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, which when clamped to a real X25519 key, is all zeros, and most WireGuard clients will refuse to use such a private key. Also, a WireGuard connection configured with a dummy private key will work only if the other end of the connection also has been configured with the corresponding dummy public key (or an attacker is MITMing it with the corresponding dummy key), so in most cases using the dummy private key with some other WireGuard client will simply result in a failure to set up the WireGuard connection.

Dummy Public Key

When using the OWG client with a dummy private key, the standard wg tool will print out the public key corresponding to the dummy private key. If you accidentally configure the remote end of a WireGuard connection with this dummy public key, the remote WireGuard client will use it without objection. This presents the same danger as above: If an adversary intercepts traffic encrypted with a dummy key, she could easily guess the key and decrypt the traffic. In that case, the adversary could also impersonate you and send and receive traffic encrypted using that dummy key on your behalf.

Socket Client Access

Any client that can read and write to the socket on which the OX agent listens (/var/run/wireguard/agent0 by default) can get the OX agent to send it legitimate shared secrets generated with the OpenPGP card’s private key. This would allow an adversary with access to the socket to simply set up her own WireGuard connection with any other WireGuard peer that had been configured to communicate with your OpenPGP card’s private key, and impersonate you to that other WireGuard peer.

Furthermore, an adversary with access to this socket would be able to decrypt traffic previously intercepted between the OWG client and other peers, as well as decrypt regular OpenPGP messages encrypted for your OpenPGP card’s decryption key.

This can mitigated to a degree by applying strict filesystem permissions to the socket, limiting the user accounts that can access the socket. If using the recommended systemd configuration from the Agent Systemd Daemon section above, only root and the members of the openpgpcard group would be able to access the socket to receive shared secrets.

Also, in the scenario where an adversary was impersonating you with an active connection, the adversary would only be to use her own WireGuard connection for as long as she had access to the socket, and the OX agent was running and had the OpenPGP card’s PIN cached, and the OpenPGP card was plugged into the computer (and, for a card with touch required to use the decryption key, you keep touching the card every 3 minutes). She wouldn’t be able to use the connection if her access to the socket was cut off, or the OX agent was stopped or the card’s PIN cleared from its cache, or the card was unplugged from the computer (or, if the card requires touch for the decryption key, you stop touching the card).

With the scenario where an adversary is attempting to decrypt previously intercepted traffic, it could also be mitigated to a degree by using a preshared key for each of your WireGuard connections. In that case, the adversary would also have to steal the preshared key for each connection in order to decrypt its traffic.

PIN Cached in Memory

In order to use a WireGuard connection where the private key is stored on an OpenPGP card, the OX agent must have access to the OpenPGP card’s PIN. The OX agent caches this PIN in memory. If an adversary is able to dump the memory of your computer while the OX agent is running and has the PIN cached, she will likely be able to determine your PIN.

Also, although the OX agent attempts to zero out all memory it uses to cache the PIN when it shuts down (or when you issue a request to clear the PIN via the OX client), the agent is a Python program, and the Python runtime may move around or create additional copies of the PIN in memory that do not get zeroed. In particular, entering the PIN via the OX client’s prompt will almost certainly create at least one copy of the PIN in memory that will not be zeroed.

Therefore, even if you clear the cached PIN from the OX agent, or shut down the agent, if an adversary is able to dump the memory of your computer after the OX agent has run, she still may be able to recover your OpenPGP card’s PIN.

Socket Server Access

If an adversary was able to replace the listener on the socket to which the OX agent normally listens (/var/run/wireguard/agent0 by default) with a program she controls, she could feed the OWG client a public key and shared secrets from a private key she controls, instead of from the OpenPGP card. If the adversary could also intercept the traffic sent by the OWG client to other WireGuard peers, she could then decrypt the traffic.

This can be mostly mitigated by making sure all other WireGuard peers that are configured to allow traffic from your peer are configured with the real public key from your OpenPGP card, and not the public key supplied by an adversary. If so configured, the worst an adversary can do is prevent the OWG client from setting up a connection with the other WireGuard peers (ie denial of service).

Hardware-only Key

While the benefit to using a WireGuard key that has been generated on an OpenPGP card is that it can’t be exfiltrated from the card, this can also be a danger. If you lose or damage the physical OpenPGP card, you will no longer be able to use any of the WireGuard connections that have been configured with the key on that card (ie denial of service).

Furthermore, an adversary with access to your OpenPGP card (including through a remote connection) can brick it simply by attempting too many failed PIN verifications. By default, 3 failed PIN verifications will block the card’s “user” PIN. The user PIN can be unblocked with the card’s reset code (also sometimes called the PUK: PIN Unblock Key). If the card has a reset code enabled (it may not by default), too many failed reset codes will block the reset code. The user PIN (and reset code) can also be unblocked with the card’s “admin” PIN. By default, 3 failed admin PIN verifications will block the admin PIN.

While the user PIN is blocked, the OpenPGP card’s keys cannot be used. If the reset code and the admin PIN also become blocked, the card’s private keys are effectively lost. The admin PIN cannot be unblocked; the card can only be “factory reset” (wiping its existing keys).

Bad Client

If the OWG client contains buggy or malicious code, it could allow an adversary to store or exfiltrate the unencrypted data you send through the WireGuard interfaces it manages. It could also potentially allow an adversary to impersonate you and send and receive traffic to and from the other WireGuard peers configured to allow it to connect.

While the OWG client only contains a few minor patches to the official WireGuard Go client, these patches haven’t been given the same deep level of scrutiny and testing that the official client has been given.

Bad Agent

If the OX agent contains buggy or malicious code, it could allow an adversary to store or exfiltrate shared secrets used by the OWG client. If the adversary could also intercept the traffic sent by the OWG client to other peers, she could use these secrets to decrypt the intercepted traffic. A buggy or malicious agent could also potentially allow an adversary to decrypt regular OpenPGP messages encrypted for your OpenPGP card’s decryption key, or sign messages using your card’s signing or authentication keys.

Also, a buggy or malicious OX agent could reveal your OpenPGP card’s PIN to an adversary.

This can be mitigated to a degree by running the OX agent as a systemd service using the full set of recommended hardening settings, which can help limit the damage that the agent could do. The agent’s source code is also fairly small (less than 3K, including extensive comments and whitespace) — although it relies heavily on the pcsc-lite daemon and the pyscard Python library for the bulk of its functionality.

See Also

For another mechanism of using private keys embedded on a smart card with WireGuard, see the WireGuard-HSM and pkclient projects by Peter Van Eenoo; together they allow a patched WireGuard Go client to use the X25519 key of a NitroKey Start via the NitroKey’s PKCS #11 interface.

For a simpler mechanism of protecting your WireGuard keys with an OpenPGP card, see the 3 Ways to Protect WireGuard with YubiKey article.