keyfork: begin work on provisioner
This commit is contained in:
parent
8e7b4b90bf
commit
f91ca2f709
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
```
|
|
@ -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
|
|
@ -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!()
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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>,
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use clap::Parser;
|
||||
|
||||
mod config;
|
||||
mod cli;
|
||||
|
||||
fn main() {
|
||||
|
|
Loading…
Reference in New Issue