rsoct - a SOP tool for OpenPGP card
rsoct (rsop-oct) is a special purpose Stateless OpenPGP (SOP) CLI tool, for use with OpenPGP card devices.
SOP exposes a simple, standardized CLI interface for a set of common OpenPGP operations.
rsoct is a special-purpose alternative to the rsop CLI tool.
It features native support for private key operations on OpenPGP card devices, and is based on a stack of rPGP, openpgp-card-rpgp and rpgpie.
The Stateless OpenPGP (SOP) CLI interface
The stateless OpenPGP command line interface (SOP) is an implementation-agnostic standard for handling OpenPGP messages and key material. rsoct is one (partial) implementation of this standard.
Stateless OpenPGP tools (like rsoct) are well suited for scripting use cases.
For more information about SOP, see https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/.
For the basic rsop tool see https://crates.io/crates/rsop. It supports private key operations based on software-based private key material (where key material is handled on the host computer, as opposed to in a hardware security device).
Installing
rsoct can be built and installed from its Rust sources with cargo:
$ cargo install rsop-oct
Alternatively, you can check for rsop-oct in your system's package manager.
OpenPGP card support
rsoct natively supports use of secret key material on OpenPGP card devices.
rsoct uses certificates in place of TSKs
In all places where SOP normally requires "KEYS" objects (aka OpenPGP "transferable secret keys"), rsoct instead requires certificates (aka OpenPGP "(transferable) public keys").
These certificate(s) are used by rsoct to search for an OpenPGP card slot that contains the associated private key material.
(Note that OpenSSH also uses this convention of using public keys as a lookup hint to find hardware-backed private key material. See for example here.)
PIN input
OpenPGP card devices require a User PIN to perform cryptographic operations.
rsoct supports two methods for providing the User PIN to cards:
- The openpgp-card-state library for User PIN handling.
rsoctcan obtain User PINs via any of the backends supported by openpgp-card-state. - Explicitly provided User PINs via the SOP
--with-key-passwordparameter (inrsoct, this parameter is interpreted as the User PIN for the OpenPGP card.)
With openpgp-card-state
By default, rsop-oct includes support for the openpgp-card-state library for User PIN handling.
In this mode, rsop-oct can automatically obtain User PINs from the openpgp-card-state backend, and doesn't require explicitly provided PINs.
With directly provided PINs
Alternatively, rsop-oct can be used completely standalone, without relying on the openpgp-card-state crate.
To build a version of rsop-oct without support for openpgp-card-state, the features settings can be adjusted like this:
$ cargo install --path . --no-default-features --features="cli"
In this mode, the User PIN must be provided for all private key operations, via the usual SOP parameter --with-key-password (analogous to a password that is used to lock a software key).
Example usage
To demonstrate the use of rsoct with an OpenPGP card, we'll do a walkthrough of a complete demonstration run here.
We'll start with a spare card that we can overwrite and provision.
If you already have a provisioned OpenPGP card with key material on it, you can skip down to the "decryption" and "signing" sections, and use your existing key material.
In addition to rsoct, this example run uses:
- the
rsoptool to generate a new OpenPGP private key, and - the
octtool (from the openpgp-card-tools crate) to initialize our OpenPGP card.
Generating a test key on the host computer
First, we generate a new private key for our test user, Alice. We use the file extension .tsk to signify that the file contains a Transferable Secret Key (or "OpenPGP private/secret key").
$ rsop generate-key "<alice@example.org>" > alice.tsk
Then, we extract a certificate from the TSK file. That is, we extract the equivalent "public key" representation of Alice's TSK, which omits the private key material. We store the resulting certificate with the file extension .cert:
$ rsop extract-cert < alice.tsk > alice.cert
Initializing a test card
Importing private key material into our OpenPGP card
Now, we plug in our test OpenPGP card and check its identity:
$ oct list
Available OpenPGP cards:
FFFE:57011137
The card we're using in this example has the identity FFFE:57011137 (this card contains no keys that we care about, so we are happy to use it. The following step entirely overwrites the card's contents!)
So first, we'll factory-reset the card, to start from a blank slate. This command removes any key material from the card, and resets the User and Admin PIN to their default values:
$ oct system factory-reset --card FFFE:57011137
Resetting Card FFFE:57011137
Now we import Alice's key material onto our test-card:
$ oct admin --card FFFE:57011137 import alice.tsk
Enter Admin PIN:
The default Admin PIN on most OpenPGP card devices is 12345678. We entered this PIN at the prompt above.
We can have a look at the newly imported key material on the card, now:
$ oct status
OpenPGP card FFFE:57011137 (card version 2.0)
Signature key:
Fingerprint: 26FD 6C05 D8AB 6D9A 7A27 A5CA DB2E 1E31 FB8E 9EA7
Creation Time: 2024-04-08 16:26:54 UTC
Algorithm: Ed25519 (EdDSA)
Signatures made: 0
Decryption key:
Fingerprint: 4B8D 7AE1 D4DE 65CE F0A8 4D2E A60A B338 5999 2476
Creation Time: 2024-04-08 16:26:54 UTC
Algorithm: Cv25519 (ECDH)
Authentication key:
Fingerprint: [unset]
Algorithm: RSA 2048 [e 32]
Remaining PIN attempts: User: 3, Admin: 3, Reset Code: 3
Configuring the User PIN for the test card with openpgp-card-state
Now we need to store the User PIN for our test card in a mechanism that openpgp-card-state can access. For this test, we'll just store the User PIN in a plain text config file. The easiest way to store the User PIN is to add the following content to the openpgp-card-state config file (you need to adjust the value for ident to reflect your own card's identity, if you're playing along at home):
[[cards]]
ident = "FFFE:57011137"
[cards.pin_storage]
Direct = "123456"
Note that after the factory-reset above, the User PIN of cards is typically 123456. For this test run, we don't change the User PIN (this is obviously not good practice for production cards), and just store this User PIN value in the openpgp-card-state configuration file (using the "Direct" PIN storage backend, which means the PIN is stored directly in the config file, as plain text).
On Linux systems the openpgp-card-state config file is typically located in ~/.config/openpgp-card-state/config.toml. It can be changed with any editor.
When using an OpenPGP card in production, with valuable key material on it, you might want to consider using a different PIN storage backend. See the documentation for openpgp-card-state for more details about this.
Using an initialized OpenPGP card for decryption or signing
Decryption on the card
We have now completed provisioning our card. In this example we will use the key material on the card to decrypt a message that was encrypted to Alice's certificate.
So first, we encrypt a message to Alice. To do this, we use Alice's public key material, from Alice's certificate alice.cert:
$ echo "hello alice" | rsop encrypt alice.cert > alice.msg
Now, we can decrypt the message based on just the public key material for Alice. Notice that we're giving rsoct the certificate file alice.cert:
$ cat alice.msg | rsoct decrypt alice.cert
hello alice
Note that without using an OpenPGP card, this would not work! For software key-based operation, decryption needs the private key material from alice.tsk.
When using rsoct to perform private key operations on an OpenPGP card (that is: decryption or signing), a number of things happen in the background:
- All OpenPGP cards that are plugged into your system are enumerated.
- rsoct checks if any of the cards contains the private key material that matches the subkey in
alice.certthat is relevant to the operation. - Once a card with suitable key material is found, rsoct uses that card's identifier to look up the card's User PIN via the
openpgp-card-stateconfiguration (note that the PIN can be stored in different backends, which use different storage mechanisms and provide different security guarantees). - If the user PIN is stored via
openpgp-card-state, rsoct obtains it. - The User PIN is presented to the card for verification. This authorizes the requested cryptographic operation.
- The cryptographic operation is performed on the card.
Signing on the card
Analogously, it's possible to produce a cryptographic signature with an OpenPGP card, using rsoct:
$ echo "hello world" | rsoct inline-sign alice.cert > sig.alice
Note that, as above, the public key data in alice.cert is not by itself sufficient to issue a signature. As above for decryption, rsoct searches for an OpenPGP card device that contains the private signing key material which corresponds to the signing subkey in alice.cert, and asks the card to issue a signature with that key.
Once produced, anyone can verify this signature as usual, by checking its validity against Alice's public key material in the certificate alice.cert:
$ cat sig.alice | rsop inline-verify alice.cert
hello world
Overview of building blocks
flowchart TD
RSOP-OCT["rsop-oct <br/> (SOP CLI tool)"] --> RPGPIE
RPGPIE["rpgpie <br/> (OpenPGP semantics library)"] --> RPGP
RPGP["rPGP <br/> (OpenPGP implementation)"]
RSOP-OCT --> OCR["openpgp-card-rpgp"] --> RPGP
RSOP-OCT --> OCS["openpgp-card-state <br/> (OpenPGP card User PIN handling)"]
OCR --> OC["openpgp-card"]
RSOP-OCT --> SOP["sop <br/> (generic SOP CLI implementation)"]
Rust SOP interface
The rsoct CLI tool is built using the excellent https://crates.io/crates/sop framework. The rsoct binary itself is trivially derived from implementations of its traits.
Warning, early-stage project!
rsoct (and its foundational library rpgpie) is in an early development stage. Use with caution!
In particular, error handling is not sufficiently elaborated yet. Failures are often not explained in sufficient detail.
Public-key operations are not currently handled in rsoct. Ideally, this support will be added in the future, but there is currently no timeline for it.