PIV Agent must be installed and set up to work with your hardware security device before use.
This is the multi-page printable view of this section. Click here to print.
PIV Agent Documentation
- 1: Install
- 2: Setup
- 3: Use
- 4: FAQ
- 5: GPG Walkthrough
1 - Install
Prerequisites
Consider redundancy
If you lose access to your hardware security device (for example if it is lost, stolen, or broken) there is no way to recover the keys stored on it. For that reason it is highly recommended that you use fallback SSH or GPG keyfiles and/or multiple hardware security devices.
Install pcsclite
piv-agent
has transitive dependencies through piv-go
, on pcsclite
.
# debian / ubuntu
sudo apt install libpcsclite1
# TODO: other platforms
...
Install piv-agent
Download the latest release, and extract it to a temporary location.
Linux
Copy the piv-agent
binary into your $PATH
, and the systemd
unit files to the correct location:
sudo cp piv-agent /usr/local/bin/
cp deploy/systemd/piv-agent.{socket,service} ~/.config/systemd/user/
systemctl --user daemon-reload
macOS
piv-agent
requires Homebrew in order to install dependencies.
So install that first.
Copy the piv-agent
binary into your $PATH
, and the launchd
.plist
files to the correct location:
sudo cp piv-agent /usr/local/bin/
cp deploy/launchd/com.github.smlx.piv-agent.plist ~/Library/LaunchAgents/
From what I can tell .plist
files only support absolute file paths, even for user agents.
So edit ~/Library/LaunchAgents/com.github.smlx.piv-agent.plist
and update the path to $HOME/.gnupg/S.gpg-agent
.
If you plan to use gpg
, install it via brew install gnupg
.
If not, you still need a pinentry
, so brew install pinentry
.
If ~/.gnupg
doesn’t already exist, create it.
mkdir ~/.gnupg
chmod 700 ~/.gnupg
Then enable the service:
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.github.smlx.piv-agent.plist
launchctl enable gui/$UID/com.github.smlx.piv-agent
A socket should appear in ~/.gnupg/S.gpg-agent
.
Disable ssh-agent
to avoid SSH_AUTH_SOCK
environment variable conflict.
launchctl disable gui/$UID/com.openssh.ssh-agent
Set launchd
user path to include /usr/local/bin/
for pinentry
.
sudo launchctl config user path $PATH
Reboot and log back in.
Socket activation
piv-agent
relies on socket activation, and is currently tested with systemd
on Linux, and launchd
on macOS.
It doesn’t listen to any sockets directly, and instead requires the init system to pass file descriptors to the piv-agent
process after it is running.
This requirement makes it possible to exit the process when not in use.
ssh-agent
and gpg-agent
functionality are enabled by default in the systemd
and launchd
configuration files.
On Linux, the index of the sockets listed in piv-agent.socket
are indicated by the arguments to --agent-types
.
2 - Setup
Hardware
Default setup
Warning
This procedure resets the state of the PIV applet to factory defaults and wipes any existing keys from all PIV slots.This procedure is only required once per hardware security device. Performing it a second time will reset the keys on the PIV applet of the device. It will not make any changes to applets providing other functionality the device may have, such as WebAuthn.
By default, piv-agent
uses six slots on your hardware security device to set up three signing keys, and three decrypting key.
Each of the signing and decrypting keys have different touch policies: never required, cached (for 15 seconds), and always.
The three signing keys are used for both SSH and GPG signing. The decrypting keys are used for GPG decryption. Having a range of touch policies available facilitates practical use of the hardware security device.
The default slot usage by piv-agent
is detailed in the table below, with reference to the Yubikey certificate slot usage description.
It is highly recommended to use these setup defaults as this has had the most usability testing.
Slot ID | Nominal purpose | piv-agent usage | Touch policy |
---|---|---|---|
0x9a | PIV Authentication | Signing | Cached |
0x9c | Digital Signature | Signing | Always |
0x9e | Card Authentication | Signing | Never |
0x9d | Key Management | Decrypting | Cached |
0x82 | Key Management (retired) | Decrypting | Always |
0x83 | Key Management (retired) | Decrypting | Never |
Example setup workflow
# find the name of the hardware security devices (cards)
piv-agent list
# generate new keys (PIN will be requested via interactive prompt)
piv-agent setup --card='Yubico YubiKey FIDO+CCID 01 00'
# view newly generated keys (SSH only by default)
piv-agent list
Single slot setup
Warning
piv-agent
has been designed to work best with the default setup.
Only set up single slots if you know what you are doing.
This action can be destructive. If you reset a slot which already contains a key, that key will be lost.
It is possible to set up a single PIV slot on your hardware device without resetting the PIV applet entirely. This means that you can target a single slot to set up a key if the slot has not been set up yet, or reset a key if the slot already contains one. Other PIV slots will not be affected, and will retain their existing keys.
For example this command will reset just the decrypting key with touch policy never
on your Yubikey:
piv-agent setup-slots --card="Yubico YubiKey FIDO+CCID 01 00" --pin=123456 --decrypting-keys=never --reset-slots
See the interactive help for more usage details:
piv-agent setup-slots --help
SSH
List keys
List your hardware SSH keys:
piv-agent list
Add the public SSH key with the touch policy you want from the list, to any SSH service.
Set SSH_AUTH_SOCK
Export the SSH_AUTH_SOCK
variable in your shell.
export SSH_AUTH_SOCK=$XDG_RUNTIME_DIR/piv-agent/ssh.socket
List keys using ssh-add
Confirm that ssh-add
can talk to piv-agent
by listing the keys available.
ssh-add -L
You should see the Yubikey ssh keys listed.
Prefer keys on the hardware security device
If you don’t already have one, it’s a good idea to generate an ed25519
keyfile and add that to all SSH services too for redundancy.
piv-agent
will automatically load and use ~/.ssh/id_ed25519
as a fallback.
By default, ssh
will offer keyfiles it finds on disk before those from the agent.
This is a problem because piv-agent
is designed to offer keys from the hardware token first, and only fall back to local keyfiles if token keys are refused.
To get ssh
to offer hardware keys first instead, copy the output of the hardware keys you want to offer from the ssh-add -L
command to a local file:
# list keys
ssh-add -L
# add output to local file
ssh-add -L | grep cached > ~/.ssh/id_yk_cached.pub
And add a line referencing the file to your ssh_config
.
IdentityFile ~/.ssh/id_yk_cached.pub
GPG
Export fallback cryptographic keys
Private GPG keys to be used by piv-agent
must be exported to the directory ~/.gnupg/piv-agent.secring/
.
Note
This step requiresgpg-agent
to be running, not piv-agent
.
See the FAQ for how to switch between the two services.Note
If your private key is encrypted using a password (it should be!), the encryption is retained during export. The key is still stored encrypted in the exported keyfile - it’s just converted into a standard OpenPGP format thatpiv-agent
can read.# example
# set umask for user-only permissions
umask 77
mkdir -p ~/.gnupg/piv-agent.secring
gpg --export-secret-key 0xB346A434C7652C02 > ~/.gnupg/piv-agent.secring/art@example.com.gpg
Disable gpg-agent
It is not possible to set a custom path for the gpg-agent
socket in a similar manner to ssh-agent
.
Instead gpg-agent
always uses a hard-coded path for its socket.
In order for piv-agent
to work with gpg
, it sets up a socket in this same default location.
To avoid conflict over this path, gpg-agent
should be disabled.
This is how you can disable gpg-agent
on Debian/Ubuntu:
- Add
no-autostart
to~/.gnupg/gpg.conf
. systemctl --user disable --now gpg-agent.socket gpg-agent.service; pkill gpg-agent
Other platforms may have slightly different instructions - PRs welcome.
Import public cryptographic keys from the security hardware
Before any private GPG keys on the hardware dvice can be used, gpg
requires their public keys to be imported.
This structure of a GPG public key contains a User ID packet, which must be signed by the associated private key.
The piv-agent list
command can synthesize a public key for the private key stored on the security hardware device.
Listing a GPG key via piv-agent list --key-formats=gpg
will require a touch to perform signing on the keys associated with those slots (due to the User ID packet).
You should provide a name and email which will be embedded in the synthesized public key (see piv-agent --help list
).
# example
piv-agent list --key-formats=ssh,gpg --pgp-name='Art Vandelay' --pgp-email='art@example.com'
Paste the public key(s) you would like to use into a key.asc
file, and run gpg --import key.asc
.
GPG Advanced
If you have followed the setup instructions to this point you should have a functional gpg-agent
backed by a PIV hardware device.
The following instructions allow deeper integration of the hardware with existing GPG keys and workflows.
Add cryptographic key stored in hardware as a GPG signing subkey
Note
There is a bug in current versions of GnuPG which doesn’t allow ECDSA keys to be added as signing subkeys. This is unfortunate since signing is much more useful than decryption.
Until this is fixed upstream, here is a Docker image containing a patched version of gpg
which will add ECDSA keys as signing subkeys.
Adding a piv-agent
OpenPGP key as a signing subkey of an existing OpenPGP key is a convenient way to integrate a hardware security device with your existing gpg
workflow.
This allows you to do things like sign git
commits using your Yubikey, while keeping the same OpenPGP key ID.
Adding a subkey requires cross-signing between the master key and sub key, so you need to export the master secret key of your existing OpenPGP key as described above to make it available to piv-agent
.
gpg
will choose the newest available subkey to perform an action. So it will automatically prefer a newly added piv-agent
subkey over any existing keyfile subkeys, but fall back to keyfiles if e.g. the Yubikey is not plugged in.
See the GPG Walkthrough for an example of this procedure.
3 - Use
Start piv-agent.socket
Start the agent sockets, and test:
systemctl --user enable --now piv-agent.socket
ssh-add -l
gpg -K
This should be enough to allow you to use piv-agent
.
Common operations
List keys
piv-agent list
If this command returns an empty list, it may be because the running agent is holding a transaction to the hardware security device. The solution is to stop the agent and run the list command again.
systemctl --user stop piv-agent
# should work now..
piv-agent list
Advanced
This section describes some ways to enhance the usability of piv-agent
.
PIN / Passphrase caching
If your pinentry supports caching credentials, piv-agent
will offer to cache the PIN of the hardware security device.
It will not cache the passphrase of any fallback keys.
This is a usability/security tradeoff that ensures that at least the encrypted private key file and its passphrase aren’t stored together on disk. It also has the advantage of ensuring that you don’t forget your keyfile passphrase, as you’ll need to enter it periodically.
However you might also forget your device PIN, so maybe don’t cache that either if you’re concerned about that possibility.
4 - FAQ
How do I switch between gpg-agent and piv-agent
Linux (systemd)
Stop both gpg-agent
and piv-agent
:
Note
Thepkill
is required because gpg
may be configured to automatically start gpg-agent
in a manner which is not managed by systemd
.systemctl --user stop gpg-agent.socket gpg-agent.service piv-agent.socket piv-agent.service; pkill gpg-agent
Start piv-agent
sockets:
systemctl --user start piv-agent.socket
Or start gpg-agent
socket:
systemctl --user start gpg-agent.socket
macOS (launchd)
Stop piv-agent
:
launchctl disable gui/$UID/com.github.smlx.piv-agent
Start piv-agent
sockets:
launchctl enable gui/$UID/com.github.smlx.piv-agent
5 - GPG Walkthrough
Overview
GnuPG being a complex piece of software, setup with piv-agent
is a bit fiddly.
This example is intended to illustrate how piv-agent
can integrate with existing GnuPG keys and workflows.
Note
This example requires switching betweengpg-agent
and piv-agent
.
See the FAQ for how to do that.Setup
Suppose I have an existing RSA OpenPGP key that I use with gpg
.
Creation of a gpg
key is outside the scope of this document, but there are reasonable instructions here.
With gpg-agent
running, listing the RSA key looks something like this:
$ gpg --list-secret-keys --keyid-format=long --with-keygrip
/home/scott/.gnupg/pubring.kbx
------------------------------
sec rsa3072/EC26B2E4240DD2A9 2021-10-17 [SC]
9FA216008BDF1AE5E1BCAEC3EC26B2E4240DD2A9
Keygrip = C284C191A1EA87796F4FE7159DD274A5D6CEADCC
uid [ultimate] Scott Leggett (piv-agent documentation example) <scott@sl.id.au>
ssb rsa3072/42B99C3339C9FBC1 2021-10-17 [E]
Keygrip = 5B918C31D4419A0D69873CB6562635C68211B872
Now we can add cryptographic subkeys stored on the Yubikey, to this RSA key, for use with piv-agent
.
Export RSA keyfiles
Lets export the private keys of the existing RSA keypairs so that they can be used in a fallback capacity by piv-agent
:
umask 77; mkdir -p ~/.gnupg/piv-agent.secring
gpg --export-secret-key 0xEC26B2E4240DD2A9 > ~/.gnupg/piv-agent.secring/EC26B2E4240DD2A9.gpg
Setup Yubikey
Now lets set up the Yubikey with new cryptographic keys.
# get the name of the card
$ piv-agent list
Security keys (cards):
Yubico YubiKey FIDO+CCID 01 00
...
# use the card name to setup the Yubikey
$ piv-agent setup --card="Yubico YubiKey FIDO+CCID 01 00" --pin=123456 --reset-security-key
List the keys that were just generated. This command will require entering the pin specified above, and touching the device twice.
Note
You might want to customize the UserID embedded in the public keys using--pgp-name
and --pgp-email
.
See piv-agent list --help
.$ piv-agent list --key-formats=gpg
Security keys (cards):
Yubico YubiKey FIDO+CCID 01 00
Signing GPG Keys:
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Yubico YubiKey FIDO+CCID 01 00 #11577026, touch policy: always
xlIEYWvCQBMIKoZIzj0DAQcCAwSfEgRY/gnycErhFQMiij9SWlNZdkVKPPHRum8k
vnY1iE8kddErPVECabFGA22RRxaf/OJ5j9TLeGu3dTWPc2hPzUxwaXYtYWdlbnQg
KHBpdi1hZ2VudCBzaWduaW5nIGtleTsgdG91Y2gtcG9saWN5IGFsd2F5cykgPG5v
cmVwbHlAZXhhbXBsZS5jb20+wmEEExMIABMFAmFrwkAJEDSxvJa0+5T5AhsDAACh
mgD/W0BCIX0tnb2FyRfyvqpdf1245K+50UjegNrADmJkNJwA/RaELw5wd7UVNsln
/mef4Qwjp5HY6Rf6MM+uBCJ4gyT2
=CUFl
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Yubico YubiKey FIDO+CCID 01 00 #11577026, touch policy: never
xlIEYWvCQBMIKoZIzj0DAQcCAwRRAjG1CSVLz55xWr7yA19Fw4uJQrRLEgCzB8f+
1EpM/gEM54VpcUZgr6+cIkRUwuU+lIOdlQhReQv9mqPWdcK5zUtwaXYtYWdlbnQg
KHBpdi1hZ2VudCBzaWduaW5nIGtleTsgdG91Y2gtcG9saWN5IG5ldmVyKSA8bm9y
ZXBseUBleGFtcGxlLmNvbT7CYQQTEwgAEwUCYWvCQAkQYerbn6tx7bECGwMAAKg3
AQDwbcR4ZklLha63wZwLYDkO4CNwRw8m8595OoabXq2g9QEAtU9MErWpO7un6GGG
tmEz6vJ2n1aPlNzxEFWkJHlq0F4=
=KYaq
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Yubico YubiKey FIDO+CCID 01 00 #11577026, touch policy: cached
xlIEYWvCQBMIKoZIzj0DAQcCAwQ3pyIrqKjEdG3fqtxzJwlhsavnOzDxRsP4ttnz
Jvj20ilmWVEwuy9SRraL40KMAf//LbtsfDF7JaPIsrKTDFN2zUxwaXYtYWdlbnQg
KHBpdi1hZ2VudCBzaWduaW5nIGtleTsgdG91Y2gtcG9saWN5IGNhY2hlZCkgPG5v
cmVwbHlAZXhhbXBsZS5jb20+wmEEExMIABMFAmFrwkAJEAJzIXQG9KHGAhsDAACf
mAD+O9CAKvL52t8FNM1OrfLXBiKNibaYAb46Xk+9cHlYm90A/2OiyDBkz1fbJoEk
1Lg4AaxcNwsmPoVRMeBCXZtIndrB
=8dJl
-----END PGP PUBLIC KEY BLOCK-----
Decrypting GPG Keys:
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Yubico YubiKey FIDO+CCID 01 00 #11577026, touch policy: never
xlIEYWvCQBMIKoZIzj0DAQcCAwTHYPdFNeoy25gUFmfpi+8UYSmfWPY/YhVbwddx
ANiAQk5+nKOoAt7oucyo2IJZMgs8Rst3NLtDCDXMhPZhpBqqzU5waXYtYWdlbnQg
KHBpdi1hZ2VudCBkZWNyeXB0aW5nIGtleTsgdG91Y2gtcG9saWN5IG5ldmVyKSA8
bm9yZXBseUBleGFtcGxlLmNvbT7CYQQTEwgAEwUCYWvCQAkQFbVY84tuH9gCGwMA
ABxTAQCFK2wLxDhU5LzetlVZhTKIBi9d9h8y3/qucrZfJ/9PUQD8DG2P+S7eGSiR
blIZt6TzPLANPgND/rsiRE/Fae9VcqE=
=X7df
-----END PGP PUBLIC KEY BLOCK-----
$
Import Yubikey cryptographic keys
Import the public keys for the slots you are interested in, into gpg
.
gpg --import <<EOF
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Yubico YubiKey FIDO+CCID 01 00 #11577026, touch policy: never
xlIEYWvCQBMIKoZIzj0DAQcCAwRRAjG1CSVLz55xWr7yA19Fw4uJQrRLEgCzB8f+
1EpM/gEM54VpcUZgr6+cIkRUwuU+lIOdlQhReQv9mqPWdcK5zUtwaXYtYWdlbnQg
KHBpdi1hZ2VudCBzaWduaW5nIGtleTsgdG91Y2gtcG9saWN5IG5ldmVyKSA8bm9y
ZXBseUBleGFtcGxlLmNvbT7CYQQTEwgAEwUCYWvCQAkQYerbn6tx7bECGwMAAKg3
AQDwbcR4ZklLha63wZwLYDkO4CNwRw8m8595OoabXq2g9QEAtU9MErWpO7un6GGG
tmEz6vJ2n1aPlNzxEFWkJHlq0F4=
=KYaq
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Yubico YubiKey FIDO+CCID 01 00 #11577026, touch policy: never
xlIEYWvCQBMIKoZIzj0DAQcCAwTHYPdFNeoy25gUFmfpi+8UYSmfWPY/YhVbwddx
ANiAQk5+nKOoAt7oucyo2IJZMgs8Rst3NLtDCDXMhPZhpBqqzU5waXYtYWdlbnQg
KHBpdi1hZ2VudCBkZWNyeXB0aW5nIGtleTsgdG91Y2gtcG9saWN5IG5ldmVyKSA8
bm9yZXBseUBleGFtcGxlLmNvbT7CYQQTEwgAEwUCYWvCQAkQFbVY84tuH9gCGwMA
ABxTAQCFK2wLxDhU5LzetlVZhTKIBi9d9h8y3/qucrZfJ/9PUQD8DG2P+S7eGSiR
blIZt6TzPLANPgND/rsiRE/Fae9VcqE=
=X7df
-----END PGP PUBLIC KEY BLOCK-----
EOF
gpg: key 61EADB9FAB71EDB1: public key "piv-agent (piv-agent signing key; touch-policy never) <noreply@example.com>" imported
gpg: key 15B558F38B6E1FD8: public key "piv-agent (piv-agent decrypting key; touch-policy never) <noreply@example.com>" imported
gpg: Total number processed: 2
gpg: imported: 2
Listing the public keys known to gpg
now shows the new keys.
$ gpg --list-keys --keyid-format=long --with-keygrip
/home/scott/.gnupg/pubring.kbx
------------------------------
pub rsa3072/EC26B2E4240DD2A9 2021-10-17 [SC]
9FA216008BDF1AE5E1BCAEC3EC26B2E4240DD2A9
Keygrip = C284C191A1EA87796F4FE7159DD274A5D6CEADCC
uid [ultimate] Scott Leggett (piv-agent documentation example) <scott@sl.id.au>
sub rsa3072/42B99C3339C9FBC1 2021-10-17 [E]
Keygrip = 5B918C31D4419A0D69873CB6562635C68211B872
pub nistp256/61EADB9FAB71EDB1 2021-10-17 [SC]
C0DDA160CE064B915F85611C61EADB9FAB71EDB1
Keygrip = 635FB47CEDA6B1C52F6E13AC5CC83629CB740CA1
uid [ unknown] piv-agent (piv-agent signing key; touch-policy never) <noreply@example.com>
pub nistp256/15B558F38B6E1FD8 2021-10-17 [SC]
4AB8F06DBC18A54D056D15F315B558F38B6E1FD8
Keygrip = 2925C2C0CAA1752F6F162BD68786EF020CF464F8
uid [ unknown] piv-agent (piv-agent decrypting key; touch-policy never) <noreply@example.com>
But no secret keys yet.
$ gpg --list-secret-keys --keyid-format=long --with-keygrip
/home/scott/.gnupg/pubring.kbx
------------------------------
sec rsa3072/EC26B2E4240DD2A9 2021-10-17 [SC]
9FA216008BDF1AE5E1BCAEC3EC26B2E4240DD2A9
Keygrip = C284C191A1EA87796F4FE7159DD274A5D6CEADCC
uid [ultimate] Scott Leggett (piv-agent documentation example) <scott@sl.id.au>
ssb rsa3072/42B99C3339C9FBC1 2021-10-17 [E]
Keygrip = 5B918C31D4419A0D69873CB6562635C68211B872
Stop gpg-agent
, start piv-agent
, and list secret keys again.
Now the cryptographic keys stored on the Yubikey are available.
$ gpg --list-secret-keys --keyid-format=long --with-keygrip
/home/scott/.gnupg/pubring.kbx
------------------------------
sec rsa3072/EC26B2E4240DD2A9 2021-10-17 [SC]
9FA216008BDF1AE5E1BCAEC3EC26B2E4240DD2A9
Keygrip = C284C191A1EA87796F4FE7159DD274A5D6CEADCC
uid [ultimate] Scott Leggett (piv-agent documentation example) <scott@sl.id.au>
ssb rsa3072/42B99C3339C9FBC1 2021-10-17 [E]
Keygrip = 5B918C31D4419A0D69873CB6562635C68211B872
sec nistp256/61EADB9FAB71EDB1 2021-10-17 [SC]
C0DDA160CE064B915F85611C61EADB9FAB71EDB1
Keygrip = 635FB47CEDA6B1C52F6E13AC5CC83629CB740CA1
uid [ unknown] piv-agent (piv-agent signing key; touch-policy never) <noreply@example.com>
sec nistp256/15B558F38B6E1FD8 2021-10-17 [SC]
4AB8F06DBC18A54D056D15F315B558F38B6E1FD8
Keygrip = 2925C2C0CAA1752F6F162BD68786EF020CF464F8
uid [ unknown] piv-agent (piv-agent decrypting key; touch-policy never) <noreply@example.com>
Add decrypting subkey
Now we can add the piv-agent decrypting key as a subkey of the RSA master key.
$ gpg --expert --edit-key 0xEC26B2E4240DD2A9
gpg (GnuPG) 2.2.27; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Secret key is available.
sec rsa3072/EC26B2E4240DD2A9
created: 2021-10-17 expires: never usage: SC
trust: ultimate validity: ultimate
ssb rsa3072/42B99C3339C9FBC1
created: 2021-10-17 expires: never usage: E
[ultimate] (1). Scott Leggett (piv-agent documentation example) <scott@sl.id.au>
gpg> addkey
Please select what kind of key you want:
(3) DSA (sign only)
(4) RSA (sign only)
(5) Elgamal (encrypt only)
(6) RSA (encrypt only)
(7) DSA (set your own capabilities)
(8) RSA (set your own capabilities)
(10) ECC (sign only)
(11) ECC (set your own capabilities)
(12) ECC (encrypt only)
(13) Existing key
(14) Existing key from card
Your selection? 13
Enter the keygrip: 2925C2C0CAA1752F6F162BD68786EF020CF464F8
Possible actions for a ECDH key: Encrypt
Current allowed actions: Encrypt
(E) Toggle the encrypt capability
(Q) Finished
Your selection? q
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0)
Key does not expire at all
Is this correct? (y/N) y
Really create? (y/N) y
sec rsa3072/EC26B2E4240DD2A9
created: 2021-10-17 expires: never usage: SC
trust: ultimate validity: ultimate
ssb rsa3072/42B99C3339C9FBC1
created: 2021-10-17 expires: never usage: E
ssb nistp256/84F7BF2FEAC32674
created: 2021-10-17 expires: never usage: E
[ultimate] (1). Scott Leggett (piv-agent documentation example) <scott@sl.id.au>
gpg> save
Add signing subkey
And we can add the piv-agent signing key as a subkey of the RSA master key too.
Note
This doesn’t currently work without a patch in GnuPG due to this GnuPG bug.
Until this is fixed upstream, here is a Docker image containing a patched version of gpg
which will add ECDSA keys as signing subkeys.
The example session below is with a patched version of gpg
.
$ gpg --expert --edit-key 0xEC26B2E4240DD2A9
gpg (GnuPG) 2.3.2; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Secret key is available.
sec rsa3072/EC26B2E4240DD2A9
created: 2021-10-17 expires: never usage: SC
trust: ultimate validity: ultimate
ssb rsa3072/42B99C3339C9FBC1
created: 2021-10-17 expires: never usage: E
ssb nistp256/84F7BF2FEAC32674
created: 2021-10-17 expires: never usage: E
[ultimate] (1). Scott Leggett (piv-agent documentation example) <scott@sl.id.au>
gpg> addkey
Please select what kind of key you want:
(3) DSA (sign only)
(4) RSA (sign only)
(5) Elgamal (encrypt only)
(6) RSA (encrypt only)
(7) DSA (set your own capabilities)
(8) RSA (set your own capabilities)
(10) ECC (sign only)
(11) ECC (set your own capabilities)
(12) ECC (encrypt only)
(13) Existing key
(14) Existing key from card
Your selection? 13
Enter the keygrip: 635FB47CEDA6B1C52F6E13AC5CC83629CB740CA1
Possible actions for this ECC key: Sign Authenticate
Current allowed actions: Sign
(S) Toggle the sign capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? q
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0)
Key does not expire at all
Is this correct? (y/N) y
Really create? (y/N) y
sec rsa3072/EC26B2E4240DD2A9
created: 2021-10-17 expires: never usage: SC
trust: ultimate validity: ultimate
ssb rsa3072/42B99C3339C9FBC1
created: 2021-10-17 expires: never usage: E
ssb nistp256/84F7BF2FEAC32674
created: 2021-10-17 expires: never usage: E
ssb nistp256/3F086B69FEE7985B
created: 2021-10-17 expires: never usage: S
[ultimate] (1). Scott Leggett (piv-agent documentation example) <scott@sl.id.au>
gpg> save
Inspect subkeys
The cryptographic keys stored on the Yubikey are now subkeys of the RSA master key.
$ gpg --list-secret-keys --keyid-format=long --with-keygrip
/home/scott/.gnupg/pubring.kbx
------------------------------
sec rsa3072/EC26B2E4240DD2A9 2021-10-17 [SC]
9FA216008BDF1AE5E1BCAEC3EC26B2E4240DD2A9
Keygrip = C284C191A1EA87796F4FE7159DD274A5D6CEADCC
uid [ultimate] Scott Leggett (piv-agent documentation example) <scott@sl.id.au>
ssb rsa3072/42B99C3339C9FBC1 2021-10-17 [E]
Keygrip = 5B918C31D4419A0D69873CB6562635C68211B872
ssb nistp256/84F7BF2FEAC32674 2021-10-17 [E]
Keygrip = 2925C2C0CAA1752F6F162BD68786EF020CF464F8
ssb nistp256/3F086B69FEE7985B 2021-10-17 [S]
Keygrip = 635FB47CEDA6B1C52F6E13AC5CC83629CB740CA1
sec nistp256/61EADB9FAB71EDB1 2021-10-17 [SC]
C0DDA160CE064B915F85611C61EADB9FAB71EDB1
Keygrip = 635FB47CEDA6B1C52F6E13AC5CC83629CB740CA1
uid [ unknown] piv-agent (piv-agent signing key; touch-policy never) <noreply@example.com>
sec nistp256/15B558F38B6E1FD8 2021-10-17 [SC]
4AB8F06DBC18A54D056D15F315B558F38B6E1FD8
Keygrip = 2925C2C0CAA1752F6F162BD68786EF020CF464F8
uid [ unknown] piv-agent (piv-agent decrypting key; touch-policy never) <noreply@example.com>
Use
Signing and encryption using the RSA master key ID will now preferentially use the cryptographic keys stored on the Yubikey, falling back to the keyfiles if the Yubikey is not available.
Specify the master key ID (e.g. 0x9FA216008BDF1AE5E1BCAEC3EC26B2E4240DD2A9
or 0xEC26B2E4240DD2A9
) to use the subkeys.
The subkey with the most recent date is preferred by gpg
.
Importantly the master key ID is the same after adding the subkeys, so any existing workflows will continue to work as before.
Publish public key
The public key can now be distributed to keyservers and other services such as Github. The subkeys are included in the exported master public key.
$ gpg --export --armor 0xEC26B2E4240DD2A9
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
FZbaoApovI7Dy6J1Ewo0vTA=
=hNHV
-----END PGP PUBLIC KEY BLOCK-----
Signing Example
With the Yubikey plugged in, the cryptographic key stored in hardware is used for signing.
$ echo bar > foo; gpg --output foo.sig --local-user 0xEC26B2E4240DD2A9 --sign foo
$ gpg --verify -v ./foo.sig
gpg: original file name='foo'
gpg: Signature made Sun 17 Oct 2021 15:48:16 AWST
gpg: using ECDSA key 28D1AAA597866ABB0B5AFC553F086B69FEE7985B
gpg: using subkey 3F086B69FEE7985B instead of primary key EC26B2E4240DD2A9
gpg: using subkey 3F086B69FEE7985B instead of primary key EC26B2E4240DD2A9
gpg: using pgp trust model
gpg: Good signature from "Scott Leggett (piv-agent documentation example) <scott@sl.id.au>" [ultimate]
gpg: using subkey 3F086B69FEE7985B instead of primary key EC26B2E4240DD2A9
gpg: binary signature, digest algorithm SHA256, key algorithm nistp256
gpg: WARNING: not a detached signature; file './foo' was NOT verified!
With the Yubikey unplugged, the traditional keyfile is used for signing.
$ gpg --verify -v ./foo.sig
gpg: original file name='foo'
gpg: Signature made Sun 17 Oct 2021 16:16:32 AWST
gpg: using RSA key 9FA216008BDF1AE5E1BCAEC3EC26B2E4240DD2A9
gpg: using pgp trust model
gpg: Good signature from "Scott Leggett (piv-agent documentation example) <scott@sl.id.au>" [ultimate]
gpg: binary signature, digest algorithm SHA512, key algorithm rsa3072
gpg: WARNING: not a detached signature; file './foo' was NOT verified!
Decrypting Example
Encryption also prefers the cryptographic key stored in hardware.
$ echo bar > foo; gpg --output foo.enc --recipient 0xEC26B2E4240DD2A9 --encrypt foo
$ gpg --decrypt -v ./foo.enc
gpg: public key is 84F7BF2FEAC32674
gpg: using subkey 84F7BF2FEAC32674 instead of primary key EC26B2E4240DD2A9
gpg: using subkey 84F7BF2FEAC32674 instead of primary key EC26B2E4240DD2A9
gpg: encrypted with 256-bit ECDH key, ID 84F7BF2FEAC32674, created 2021-10-17
"Scott Leggett (piv-agent documentation example) <scott@sl.id.au>"
gpg: AES256 encrypted data
gpg: original file name='foo'
bar
You can also specify multiple key IDs when encrypting (one keyfile, one hardware), for fallback purposes.
$ echo bar > foo; gpg --output foo.enc --recipient 0x42B99C3339C9FBC1! --recipient 0x84F7BF2FEAC32674! --encrypt foo
$ gpg --decrypt -v ./foo.enc
gpg: public key is 42B99C3339C9FBC1
gpg: using subkey 42B99C3339C9FBC1 instead of primary key EC26B2E4240DD2A9
gpg: public key is 84F7BF2FEAC32674
gpg: using subkey 84F7BF2FEAC32674 instead of primary key EC26B2E4240DD2A9
gpg: encrypted with 256-bit ECDH key, ID 84F7BF2FEAC32674, created 2021-10-17
"Scott Leggett (piv-agent documentation example) <scott@sl.id.au>"
gpg: using subkey 42B99C3339C9FBC1 instead of primary key EC26B2E4240DD2A9
gpg: encrypted with 3072-bit RSA key, ID 42B99C3339C9FBC1, created 2021-10-17
"Scott Leggett (piv-agent documentation example) <scott@sl.id.au>"
gpg: AES256 encrypted data
gpg: original file name='foo'
bar
Common software integration
git
The same master key ID will work as before, but signing will prefer to use the hardware security device if it is plugged in.
# example ~/.config/git/config
[user]
name = Scott Leggett
email = scott@sl.id.au
signingKey = 9FA216008BDF1AE5E1BCAEC3EC26B2E4240DD2A9
[commit]
gpgSign = true
pass
pass
has the ability to encrypt to multiple key-ids.
Running pass init
will re-encrypt existing passwords and configure pass
to use the specified key-ids for encryption.
As usual, piv-agent
will use the cryptographic key stored in hardware for decryption if it is available, but fall back to the keyfile otherwise.
pass init 0x42B99C3339C9FBC1! 0x84F7BF2FEAC32674!