diff --git a/keyfork-user-guide/src/SUMMARY.md b/keyfork-user-guide/src/SUMMARY.md index f5a48f5..2434a57 100644 --- a/keyfork-user-guide/src/SUMMARY.md +++ b/keyfork-user-guide/src/SUMMARY.md @@ -5,6 +5,7 @@ - [Installing Keyfork](./INSTALL.md) - [Common Usage](./usage.md) +- [Configuration File](./config-file.md) - [Binaries](./bin/index.md) - [keyfork](./bin/keyfork/index.md) - [mnemonic](./bin/keyfork/mnemonic/index.md) @@ -21,4 +22,5 @@ # Developers Guide - [Writing Binaries](./dev-guide/index.md) + - [Provisioners](./dev-guide/provisioners.md) - [Auditing](./dev-guide/auditing.md) diff --git a/keyfork-user-guide/src/config-file.md b/keyfork-user-guide/src/config-file.md new file mode 100644 index 0000000..04f1027 --- /dev/null +++ b/keyfork-user-guide/src/config-file.md @@ -0,0 +1,24 @@ +# Configuration File + +The Keyfork configuration file is used to store the integrity of the mnemonic +and record the provisioning of keys. The configuration file is created and +edited automatically when provisioning operations are performed. + +Provisioners will store the name of the provisioner used, the account index, +an optional identifier for target discovery, and any additional arbitrary +metadata the provisioner may make use of. + +```toml +# File automatically generated by `keyfork` + +[mnemonic] +hash = "67c945b48b4ab248bdbe630dd6ea0d56de3bb2ed13c3836dbcc0c04967a59b35" + +[[provisioner]] +name = "openpgp-card" +account = 0 +identifier = "0006:26144195" + +[[provisioner.metadata]] +cardholder_name = "Ryan Heywood" +``` diff --git a/keyfork-user-guide/src/dev-guide/provisioners.md b/keyfork-user-guide/src/dev-guide/provisioners.md new file mode 100644 index 0000000..cac0c7a --- /dev/null +++ b/keyfork-user-guide/src/dev-guide/provisioners.md @@ -0,0 +1,58 @@ +# 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. + +## 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. + +[application identifier]: https://docs.rs/openpgp-card-sequoia/latest/openpgp_card_sequoia/struct.Card.html#method.application_identifier +[cardholder name]: https://docs.rs/openpgp-card-sequoia/latest/openpgp_card_sequoia/struct.Card.html#method.cardholder_name diff --git a/keyfork/src/cli/mod.rs b/keyfork/src/cli/mod.rs index 11134eb..874af96 100644 --- a/keyfork/src/cli/mod.rs +++ b/keyfork/src/cli/mod.rs @@ -1,6 +1,7 @@ use clap::{Parser, Subcommand}; mod mnemonic; +mod provision; mod shard; /// The Kitchen Sink of Entropy. @@ -20,6 +21,10 @@ pub enum KeyforkCommands { /// Splitting and combining secrets using Shamir's Secret Sharing. Shard(shard::Shard), + /// Derive and deploy keys. + #[command(subcommand_negates_reqs(true))] + Provision(provision::Provision), + /// Keyforkd background daemon to manage derivation. Daemon, } @@ -35,6 +40,9 @@ impl KeyforkCommands { // TODO: When actually fleshing out, this takes a `Read` and a `Write` s.command.handle(s, keyfork)?; } + KeyforkCommands::Provision(p) => { + p.handle(keyfork)?; + } KeyforkCommands::Daemon => { todo!() } diff --git a/keyfork/src/cli/provision/mod.rs b/keyfork/src/cli/provision/mod.rs new file mode 100644 index 0000000..31f4220 --- /dev/null +++ b/keyfork/src/cli/provision/mod.rs @@ -0,0 +1,150 @@ +use super::Keyfork; +use crate::config; + +use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; + +#[derive(Debug, Clone)] +pub enum Provisioner { + OpenPGPCard(OpenPGPCard), +} + +impl std::fmt::Display for Provisioner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Provisioner::OpenPGPCard(_) => f.write_str("openpgp-card"), + } + } +} + +impl Provisioner { + fn discover(&self) -> Vec<(String, Option)> { + match self { + Provisioner::OpenPGPCard(o) => o.discover(), + } + } + + fn provision( + &self, + provisioner: config::Provisioner, + ) -> Result<(), Box> { + match self { + Provisioner::OpenPGPCard(o) => o.provision(provisioner), + } + } +} + +impl ValueEnum for Provisioner { + fn value_variants<'a>() -> &'a [Self] { + &[Self::OpenPGPCard(OpenPGPCard)] + } + + fn to_possible_value(&self) -> Option { + Some(PossibleValue::new(match self { + Self::OpenPGPCard(_) => "openpgp-card", + })) + } +} + +trait ProvisionExec { + /// Discover all known places the formatted key can be deployed to. + fn discover(&self) -> Vec<(String, Option)> { + vec![] + } + + /// Derive a key and deploy it to a target. + fn provision(&self, p: config::Provisioner) -> Result<(), Box>; +} + +#[derive(Clone, Debug)] +pub struct OpenPGPCard; + +impl ProvisionExec for OpenPGPCard { + fn discover(&self) -> Vec<(String, Option)> { + /* + vec![ + ( + "0006:26144195".to_string(), + Some("Yubicats Heywood".to_string()), + ), + ( + "0006:2614419y".to_string(), + Some("Yubicats Heywood".to_string()), + ), + ] + */ + vec![] + } + + fn provision(&self, _p: config::Provisioner) -> Result<(), Box> { + todo!() + } +} + +#[derive(Subcommand, Clone, Debug)] +pub enum ProvisionSubcommands { + /// Discover all available targets on the system. + Discover, +} + +// NOTE: All struct fields are marked as Option so they may be constructed when running a +// subcommand. This is allowed through marking the parent command subcommand_negates_reqs(true). + +#[derive(Parser, Debug, Clone)] +pub struct Provision { + #[command(subcommand)] + pub subcommand: Option, + + provisioner_name: Provisioner, + + /// Account ID. + #[arg(long, required(true))] + account_id: Option, + + /// Identifier of the hardware to deploy to, listable by running the `discover` subcommand. + #[arg(long, required(true))] + identifier: Option, +} + +// NOTE: In the future, this impl will be used by `keyfork recover` to reprovision hardware from +// the config file, iterating through a [Config::provisioner]. +// TODO: How can metadata be passed in? + +#[derive(thiserror::Error, Debug)] +#[error("Missing field: {0}")] +pub struct MissingField(&'static str); + +impl TryFrom for config::Provisioner { + type Error = MissingField; + + fn try_from(value: Provision) -> Result { + Ok(Self { + name: value.provisioner_name.to_string(), + account: value.account_id.ok_or(MissingField("account_id"))?, + identifier: value.identifier.ok_or(MissingField("identifier"))?, + metadata: Default::default(), + }) + } +} + +impl Provision { + pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box> { + match self.subcommand { + Some(ProvisionSubcommands::Discover) => { + let mut iter = self.provisioner_name.discover().into_iter().peekable(); + while let Some((identifier, context)) = iter.next() { + println!("Identifier: {identifier}"); + if let Some(context) = context { + println!("Context: {context}"); + } + if iter.peek().is_some() { + println!("---"); + } + } + } + None => { + self.provisioner_name.provision(self.clone().try_into()?)?; + } + } + Ok(()) + } +} diff --git a/keyfork/src/config.rs b/keyfork/src/config.rs new file mode 100644 index 0000000..f7112b5 --- /dev/null +++ b/keyfork/src/config.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Mnemonic { + pub hash: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Provisioner { + pub name: String, + pub account: u32, + pub identifier: String, + pub metadata: Option>, +} + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub mnemonic: Mnemonic, + pub provisioner: Vec, +} diff --git a/keyfork/src/main.rs b/keyfork/src/main.rs index 551aeeb..02e6310 100644 --- a/keyfork/src/main.rs +++ b/keyfork/src/main.rs @@ -2,6 +2,7 @@ use clap::Parser; +mod config; mod cli; fn main() {