keyfork: add `wizard generate-shard-secret`

This commit is contained in:
Ryan Heywood 2024-01-07 23:57:50 -05:00
parent 87a40f636d
commit 8792ef69e1
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
5 changed files with 545 additions and 303 deletions

653
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -19,3 +19,8 @@ tokio = { version = "1.35.1", default-features = false, features = ["rt-multi-th
keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" }
keyforkd-client = { version = "0.1.0", path = "../keyforkd-client", default-features = false, features = ["ed25519"] }
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"] }
card-backend-pcsc = "0.5.0"
openpgp-card-sequoia = "0.2.0"
openpgp-card = "0.4.1"
keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt" }
env_logger = "0.10.1"

View File

@ -5,6 +5,7 @@ mod mnemonic;
mod provision;
mod shard;
mod recover;
mod wizard;
/// The Kitchen Sink of Entropy.
#[derive(Parser, Clone, Debug)]
@ -32,6 +33,9 @@ pub enum KeyforkCommands {
/// Recover a seed using a recovery mechanism and begin the Keyfork daemon.
Recover(recover::Recover),
/// Utilities to automatically manage the setup of Keyfork.
Wizard(wizard::Wizard),
}
impl KeyforkCommands {
@ -45,7 +49,6 @@ impl KeyforkCommands {
println!("{response}");
}
KeyforkCommands::Shard(s) => {
// TODO: When actually fleshing out, this takes a `Read` and a `Write`
s.command.handle(s, keyfork)?;
}
KeyforkCommands::Provision(p) => {
@ -54,6 +57,9 @@ impl KeyforkCommands {
KeyforkCommands::Recover(r) => {
r.handle(keyfork)?;
}
KeyforkCommands::Wizard(w) => {
w.handle(keyfork)?;
}
}
Ok(())
}

163
keyfork/src/cli/wizard.rs Normal file
View File

@ -0,0 +1,163 @@
use super::Keyfork;
use clap::{Parser, Subcommand};
use card_backend_pcsc::PcscBackend;
use openpgp_card::card_do::ApplicationIdentifier;
use openpgp_card_sequoia::{
state::Open,
types::KeyType,
Card,
};
use keyfork_derive_openpgp::openpgp::{self, packet::UserID, types::KeyFlags, Cert};
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest},
DerivationIndex, DerivationPath,
};
use keyfork_prompt::{Message, PromptManager};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn derive_key(seed: &[u8], index: u8) -> Result<Cert> {
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let mut shrd_u32 = [0u8; 4];
shrd_u32[..].copy_from_slice(&"shrd".bytes().collect::<Vec<u8>>());
let account = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let subkey = DerivationIndex::new(u32::from(index), true)?;
let path = DerivationPath::default()
.chain_push(chain)
.chain_push(account)
.chain_push(subkey);
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
let response = request.derive_with_master_seed(seed.to_vec())?;
let userid = UserID::from(format!("Keyfork Shard {index}"));
let cert = keyfork_derive_openpgp::derive(response, &subkeys, &userid)?;
Ok(cert)
}
// TODO: extract into crate
/// Factory reset the current card so long as it does not match the last-used backend.
fn factory_reset_current_card(
previous_backend: &mut Option<ApplicationIdentifier>,
cert: &Cert,
) -> Result<()> {
let policy = openpgp::policy::NullPolicy::new();
let valid_cert = cert.with_policy(&policy, None)?;
let signing_key = valid_cert
.keys()
.for_signing()
.secret()
.next()
.expect("no signing key found");
let decryption_key = valid_cert
.keys()
.for_storage_encryption()
.secret()
.next()
.expect("no decryption key found");
let authentication_key = valid_cert
.keys()
.for_authentication()
.secret()
.next()
.expect("no authentication key found");
if let Some(current_backend) = PcscBackend::cards(None)?.next().transpose()? {
let mut card = Card::<Open>::new(current_backend)?;
let mut transaction = card.transaction()?;
let application_identifier = transaction.application_identifier()?;
if previous_backend.is_some_and(|pb| pb == application_identifier) {
// we were given the same card, error
panic!("Previously used card {application_identifier} was reused");
} else {
let _ = previous_backend.insert(application_identifier);
}
transaction.factory_reset()?;
let mut admin = transaction.to_admin_card("12345678")?;
admin.upload_key(signing_key, KeyType::Signing, None)?;
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
} else {
panic!("No smart card found");
}
Ok(())
}
fn generate_shard_secret(threshold: u8, max: u8, keys_per_shard: u8) -> Result<()> {
keyfork_plumbing::ensure_safe();
let seed = keyfork_plumbing::generate_entropy_of_size(256 / 8)?;
let mut pm = PromptManager::new(std::io::stdin(), std::io::stderr())?;
let mut certs = vec![];
let mut last_card = None;
for index in 0..max {
let cert = derive_key(&seed, index)?;
for i in 0..keys_per_shard {
pm.prompt_message(&Message::Text(format!(
"Please insert user {index} key {i}"
)))?;
factory_reset_current_card(&mut last_card, &cert)?;
}
certs.push(cert);
}
keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?;
Ok(())
}
#[derive(Subcommand, Clone, Debug)]
pub enum WizardSubcommands {
/// Create a 256 bit secret and shard the secret to smart cards.
///
/// Smart cards will need to be plugged in periodically during the wizard, where they will be factory reset and
/// provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered with `keyfork recover shard` or
/// `keyfork recover remote-shard`. The share file will be printed to standard output.
GenerateShardSecret {
/// The minimum amount of keys required to decrypt the secret.
#[arg(long)]
threshold: u8,
/// The maximum amount of shards.
#[arg(long)]
max: u8,
/// The amount of smart cards to provision per-shard.
#[arg(long, default_value = "1")]
keys_per_shard: u8,
},
}
impl WizardSubcommands {
fn handle(&self) -> Result<()> {
match self {
WizardSubcommands::GenerateShardSecret {
threshold,
max,
keys_per_shard,
} => generate_shard_secret(*threshold, *max, *keys_per_shard),
}
}
}
#[derive(Parser, Debug, Clone)]
pub struct Wizard {
#[command(subcommand)]
command: WizardSubcommands,
}
impl Wizard {
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
self.command.handle()?;
Ok(())
}
}

View File

@ -1,14 +1,25 @@
#![allow(clippy::module_name_repetitions)]
use std::process::ExitCode;
use clap::Parser;
mod config;
mod cli;
fn main() {
fn main() -> ExitCode {
let opts = cli::Keyfork::parse();
opts.command
.handle(&opts)
.expect("Unable to handle command");
env_logger::init();
if let Err(e) = opts.command .handle(&opts) {
println!("Unable to run command: {e}");
let mut source = e.source();
while let Some(new_error) = source.take() {
eprintln!("Source: {new_error}");
source = new_error.source();
}
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}