keyfork: add `wizard generate-shard-secret`
This commit is contained in:
parent
87a40f636d
commit
8792ef69e1
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue