keyfork: begin work on provisioner
This commit is contained in:
parent
8e7b4b90bf
commit
f91ca2f709
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
- [Installing Keyfork](./INSTALL.md)
|
- [Installing Keyfork](./INSTALL.md)
|
||||||
- [Common Usage](./usage.md)
|
- [Common Usage](./usage.md)
|
||||||
|
- [Configuration File](./config-file.md)
|
||||||
- [Binaries](./bin/index.md)
|
- [Binaries](./bin/index.md)
|
||||||
- [keyfork](./bin/keyfork/index.md)
|
- [keyfork](./bin/keyfork/index.md)
|
||||||
- [mnemonic](./bin/keyfork/mnemonic/index.md)
|
- [mnemonic](./bin/keyfork/mnemonic/index.md)
|
||||||
|
@ -21,4 +22,5 @@
|
||||||
# Developers Guide
|
# Developers Guide
|
||||||
|
|
||||||
- [Writing Binaries](./dev-guide/index.md)
|
- [Writing Binaries](./dev-guide/index.md)
|
||||||
|
- [Provisioners](./dev-guide/provisioners.md)
|
||||||
- [Auditing](./dev-guide/auditing.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};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
mod mnemonic;
|
mod mnemonic;
|
||||||
|
mod provision;
|
||||||
mod shard;
|
mod shard;
|
||||||
|
|
||||||
/// The Kitchen Sink of Entropy.
|
/// The Kitchen Sink of Entropy.
|
||||||
|
@ -20,6 +21,10 @@ pub enum KeyforkCommands {
|
||||||
/// Splitting and combining secrets using Shamir's Secret Sharing.
|
/// Splitting and combining secrets using Shamir's Secret Sharing.
|
||||||
Shard(shard::Shard),
|
Shard(shard::Shard),
|
||||||
|
|
||||||
|
/// Derive and deploy keys.
|
||||||
|
#[command(subcommand_negates_reqs(true))]
|
||||||
|
Provision(provision::Provision),
|
||||||
|
|
||||||
/// Keyforkd background daemon to manage derivation.
|
/// Keyforkd background daemon to manage derivation.
|
||||||
Daemon,
|
Daemon,
|
||||||
}
|
}
|
||||||
|
@ -35,6 +40,9 @@ impl KeyforkCommands {
|
||||||
// TODO: When actually fleshing out, this takes a `Read` and a `Write`
|
// TODO: When actually fleshing out, this takes a `Read` and a `Write`
|
||||||
s.command.handle(s, keyfork)?;
|
s.command.handle(s, keyfork)?;
|
||||||
}
|
}
|
||||||
|
KeyforkCommands::Provision(p) => {
|
||||||
|
p.handle(keyfork)?;
|
||||||
|
}
|
||||||
KeyforkCommands::Daemon => {
|
KeyforkCommands::Daemon => {
|
||||||
todo!()
|
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;
|
use clap::Parser;
|
||||||
|
|
||||||
|
mod config;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
Loading…
Reference in New Issue