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" }
|
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"] }
|
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"] }
|
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 provision;
|
||||||
mod shard;
|
mod shard;
|
||||||
mod recover;
|
mod recover;
|
||||||
|
mod wizard;
|
||||||
|
|
||||||
/// The Kitchen Sink of Entropy.
|
/// The Kitchen Sink of Entropy.
|
||||||
#[derive(Parser, Clone, Debug)]
|
#[derive(Parser, Clone, Debug)]
|
||||||
|
@ -32,6 +33,9 @@ pub enum KeyforkCommands {
|
||||||
|
|
||||||
/// Recover a seed using a recovery mechanism and begin the Keyfork daemon.
|
/// Recover a seed using a recovery mechanism and begin the Keyfork daemon.
|
||||||
Recover(recover::Recover),
|
Recover(recover::Recover),
|
||||||
|
|
||||||
|
/// Utilities to automatically manage the setup of Keyfork.
|
||||||
|
Wizard(wizard::Wizard),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyforkCommands {
|
impl KeyforkCommands {
|
||||||
|
@ -45,7 +49,6 @@ impl KeyforkCommands {
|
||||||
println!("{response}");
|
println!("{response}");
|
||||||
}
|
}
|
||||||
KeyforkCommands::Shard(s) => {
|
KeyforkCommands::Shard(s) => {
|
||||||
// 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) => {
|
KeyforkCommands::Provision(p) => {
|
||||||
|
@ -54,6 +57,9 @@ impl KeyforkCommands {
|
||||||
KeyforkCommands::Recover(r) => {
|
KeyforkCommands::Recover(r) => {
|
||||||
r.handle(keyfork)?;
|
r.handle(keyfork)?;
|
||||||
}
|
}
|
||||||
|
KeyforkCommands::Wizard(w) => {
|
||||||
|
w.handle(keyfork)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
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)]
|
#![allow(clippy::module_name_repetitions)]
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
||||||
fn main() {
|
fn main() -> ExitCode {
|
||||||
let opts = cli::Keyfork::parse();
|
let opts = cli::Keyfork::parse();
|
||||||
|
|
||||||
opts.command
|
env_logger::init();
|
||||||
.handle(&opts)
|
|
||||||
.expect("Unable to handle command");
|
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