keyfork/docs/src/dev-guide/provisioners.md

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.