OpenPGP card support in Sequoia

By Heiko | December 20, 2021

Over the last months we’ve worked on adding support for OpenPGP card hardware tokens to Sequoia. OpenPGP cards (like the free Gnuk implementation, or e.g. Nitrokey and YubiKey devices) are great when you want to use an OpenPGP key, but don’t want the private key material stored on your computer. Advanced OpenPGP users have come to expect their software to support them.

Earlier this month, we connected a set of physical cards to our continuous integration (CI) machine and configured a job to run a test suite on these cards. This setup ensures that every change to our code is tested on a set of physical OpenPGP cards. The ability to test against multiple cards is essential, as cards implement different versions of the specification, and, on top of that, many have various quirks.

Towards OpenPGP Card Support in Sequoia

Multiple OpenPGP cards are attached to a USB hub with green 
LEDs. The hub is placed on a black 19" rack server
Figure: A set of OpenPGP cards attached to our CI server.

Many advanced users of OpenPGP use an OpenPGP card to store their private key material. An OpenPGP card is a type of hardware security module (HSM), which provides an API to decrypt and sign messages, but does not provide access to the private key material itself. This prevents an attacker from exfiltrating the private key material even if they get access to a computer that the HSM is attached to.

Testing Physical Hardware

We’re now bringing OpenPGP card support to Sequoia. One of the big challenges is testing this code. With normal code, we’d write unit tests and integration tests to assert that it runs as expected. However, testing code that interacts with hardware is more challenging.

We started development by running tests using hardware tokens attached to the developer’s machine. This wasn’t ideal, because we couldn’t run the tests in CI, and every developer would need a set of tokens.

Next, we explored virtual tokens using Gnuk emulation and a Java Card simulator and we have been running our test suite against simulated YubiKey NEO and SmartPGP tokens in GitLab CI for some time now. This was a good start, but it still only covered these specific devices.

When a specification is implemented more than once, each result often differs from the other in subtle (or not so subtle) ways. This is doubly true for hardware, which can often not be changed after production, and where device drivers are typically expected to compensate for bugs or quirks.

This caveat also applies to OpenPGP smart cards, all of which in theory implement the same specification. As we built our client code and ran tests against different cards, we discovered quite a bit of diversity in their behavior.

So using just two types of simulated cards would clearly not be enough to develop a robust framework, and avoid regressions in the future. We needed a fleet of actual physical hardware tokens to use from our CI. As we already have our own CI machine, we could attach hardware to it.

To get started, we installed a USB hub that allows us to programmatically power each port on and off separately. This is essential to recover from hard errors without having someone manually replug a stuck card. Since we couldn’t figure out how to pass the USB hub into a libvirt environment, we also installed an additional USB controller connected to the PCI express bus, which we could easily pass into the VM. All USB devices in the VM are then shared with our GitLab runner container.

Right now, we use the following cards in our CI setup: Nitrokey Start, Pro 1, & Pro 2 (which were donated by Nitrokey), and a standard OpenPGP card (version 2.1) from the FLOSS shop.

If you want to donate additional OpenPGP cards (especially older and/or exotic ones) to use in our CI setup, we’d be happy to receive them.

Although our CI setup with physical OpenPGP cards is new, it has already identified an issue with our code, which our local testing setup hadn’t identified.

Unfortunately, hardware has a limited lifetime. To limit wear on the physical cards, we first run the tests on the simulated Java Card OpenPGP applications. Only if those tests pass do we run the tests on the physical tokens.

Writing tests isn’t glorious. Creating infrastructure to run tests even less so. But, we’re excited about our CI setup for testing changes to our OpenPGP card support. We’re now confident that we can make changes to this code without inadvertently breaking functionality for some types of card.


General purpose low-level library

OpenPGP card support in Sequoia will be based on the new general purpose openpgp-card set of Rust OpenPGP card client libraries.

The low-level openpgp-card crate implements the central operations for using OpenPGP cards, such as importing keys to a card, decryption and signing. (We only implement the OpenPGP card application layer; for card reader support, we by default use pcscd. This increases interoperability with other HSM-using programs.) We plan to implement all functionality defined in the standard. On top of that, we will also implement device-specific proprietary functionality, where that is practical.

Integration with Sequoia

The openpgp-card crate is not specific to Sequoia, it was designed to be usable by other OpenPGP implementations without pulling in any Sequoia-specific code.

Sequoia support is implemented separately in the openpgp-card-sequoia crate. It provides tight integration with Sequoia. In particular, it implements Sequoia’s Signer and Decryptor traits, which makes using the OpenPGP card support from Sequoia straightforward.

End-user CLI tools

You can already try out our new OpenPGP card library by using the openpgp-card-tools crate. The tools in this crate are mainly intended for key management on OpenPGP cards: they can be used to inspect the status of a card, to easily import PGP keys to a card, or to manage PINs.

The following transcript demonstrates the basic functionality:

$ cargo install openpgp-card-tools

$ opgpcard status

OpenPGP card FFFE:43194240 (card version 2.0)

Cardholder: Foo Bar
Language preferences 'en'

Signature key (Ed25519 (EdDSA))
  fingerprint: F290 DBBF 21DB 8634 3C96  157B 87BE 15B7 F548 D97C
  created: 2021-06-22 11:21:37

Decryption key (Cv25519 (ECDH))
  fingerprint: 3C6E 8F06 7613 8935 8B8D  7666 73C7 F1A9 EEDA C360
  created: 2021-06-22 11:21:37

Authentication key (Ed25519 (EdDSA))
  fingerprint: D6AA 48EF 39A2 6F26 C42D  5BCB AAD2 14D5 5332 C838
  created: 2021-09-11 11:42:43

Signature counter: 0
Signature pin only valid once: true
Password validation retry count:
  user pw: 3, reset: 3, admin pw: 3

$ echo "foo" | opgpcard sign --detached --card FFFE:43194240 --pin-file <pin file> -s <pubkey file>


(More detailed build and usage instructions are available in the project’s README file.)