4.2 KiB
Developing Provisioners
Note: This document makes heavy use of references to OpenPGP and assumes the user is familiar with the concept of storing cryptographic keys on external hardware.
A provisioner is a binary that deploys a derived key (formatted using any
particular mechanism) to an external source, such as a smart card. Provisioners
should hardcode at least one path index (such as 7366512
, for "PGP") specific
to the usage of the key to be provisioned (and such index should be recorded in
the keyfork-path-data crate), and accept at least one index to use as what
bip32 calls an "account". While some users may never practically make use of
multiple accounts, having the option to specify multiple accounts is important.
Plumbing
Provisioners should be split into two crates: one that takes a path and
generates formatted keys from that path, and one that takes the formatted keys
and deploys them to the provisioning target. An example of this are the
keyfork-derive-openpgp
and keyfork-provision-openpgp-card
crates. One
creates an OpenPGP Transferable Secret Key (TSK) which may be usable by any
application, and the other takes the TSK and deploys it to a smart card.
By themselves, these plumbing crates are not meant to be intuitive - they are meant to take a raw derivation path and as little as possible information, and perform their operations. The derivation crate should request one key from Keyforkd and, if further keys are required, derive from there. This is the case for OpenPGP, where an OpenPGP certificate may have multiple subkeys. The provisioning crate must enforce the given formatted key is of a correct format. For example, OpenPGP TSKs can contain any number of subkeys, but a TSK that is provisioned to a smart card must have exactly one signing, encryption, and authentication key. Whether or not the subkeys share this functionality between them is irrelevant - what is important is that all functionality must be available and all key deployment must be ambiguous. Additionally, provisioning plumbing must have some way to denote the target the formatted keys will be provisioned to, avoiding ambiguities when (for example) multiple devices are plugged into a host. A plumbing crate may provide a binary to list identifiers for potential targets, if possible.
Deriving Data
A quick example is provided to show how data can be requested from the Keyfork server. The response contains the bytes representing the private key; the chain code, depth, and algorithm are also included in the response, and can be used for further derivation if required.
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
DerivationIndex, DerivationPath,
};
use keyforkd_client::Client;
let path = DerivationPath::from_str("m/44'/0'/0'")?;
let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &path);
let response: DerivationResponse = Client::discover_socket()?
.request(&request.into())?
.try_into()?;
let derived_key = response.data;
Porcelain
Once the plumbing crates have been written, porcelain subcommands should be
written. If target discovery is necessary, Keyfork provides a subcommand
keyfork provision [provisioner name] discover
using the discover()
method
for a provisioner. For the case of OpenPGP cards, the command keyfork provision openpgp-card discover
could iterate through all cards available on
the system, and print both their application identifier and optionally
cardholder name if present.
The subcommand for keyfork provision
should accept the previously mentioned
account index, but should be opinionated about the keys provisioned to a
device. The porcelain provisioner code should make a best-effort attempt to
derive unique keys for each use, such as OpenPGP capabilities or PIV slots.
Additionally, when provisioning to a key, the configuration for that
provisioner should be stored to the configuration file.