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
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.
Crates
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
URL: https://keys.openpgp.org/
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>
-----BEGIN PGP MESSAGE-----
wr0EABYKAG8FgmG2eCIJEIe+Fbf1SNl8RxQAAAAAAB4AIHNhbHRAbm90YXRpb25z
LnNlcXVvaWEtcGdwLm9yZ3FmTQsKWDx+NPSQsi+dKrEClGdg+AJ5bWbdKmhtCNtw
FiEE8pDbvyHbhjQ8lhV7h74Vt/VI2XwAAOhrAQCPH9j9gPF1ppVZjGEaS/OYLUA+
cetPq9OdB8rctUrFcQEAo0OcBwFkE41wORP3QHJYwBngH2x+vVDlMwHp82dePgA=
=tN8t
-----END PGP MESSAGE-----
(More detailed build and usage instructions are available in the project’s README file.)