keyfork: begin work on provisioner

This commit is contained in:
Ryan Heywood 2023-12-18 12:19:21 -05:00
parent 8e7b4b90bf
commit f91ca2f709
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
7 changed files with 265 additions and 0 deletions

View File

@ -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)

View File

@ -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"
```

View File

@ -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

View File

@ -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!()
}

View File

@ -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<String>)> {
match self {
Provisioner::OpenPGPCard(o) => o.discover(),
}
}
fn provision(
&self,
provisioner: config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> {
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<PossibleValue> {
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<String>)> {
vec![]
}
/// Derive a key and deploy it to a target.
fn provision(&self, p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>>;
}
#[derive(Clone, Debug)]
pub struct OpenPGPCard;
impl ProvisionExec for OpenPGPCard {
fn discover(&self) -> Vec<(String, Option<String>)> {
/*
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<dyn std::error::Error>> {
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<ProvisionSubcommands>,
provisioner_name: Provisioner,
/// Account ID.
#[arg(long, required(true))]
account_id: Option<u32>,
/// Identifier of the hardware to deploy to, listable by running the `discover` subcommand.
#[arg(long, required(true))]
identifier: Option<String>,
}
// 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<Provision> for config::Provisioner {
type Error = MissingField;
fn try_from(value: Provision) -> Result<Self, Self::Error> {
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<dyn std::error::Error>> {
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(())
}
}

22
keyfork/src/config.rs Normal file
View File

@ -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<HashMap<String, String>>,
}
#[derive(Serialize, Deserialize)]
pub struct Config {
pub mnemonic: Mnemonic,
pub provisioner: Vec<Provisioner>,
}

View File

@ -2,6 +2,7 @@
use clap::Parser;
mod config;
mod cli;
fn main() {