keyfork: enum-trait-ify choose-your-own commands

This commit is contained in:
Ryan Heywood 2024-08-05 17:43:22 -04:00
parent 536e6da5ad
commit 5219c5a99f
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
2 changed files with 227 additions and 216 deletions

View File

@ -1,5 +1,5 @@
use super::Keyfork; use super::Keyfork;
use clap::{Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use keyfork_derive_openpgp::{ use keyfork_derive_openpgp::{
openpgp::{ openpgp::{
@ -27,45 +27,51 @@ pub enum DeriveSubcommands {
/// It is recommended to use the default expiration of one day and to change the expiration /// It is recommended to use the default expiration of one day and to change the expiration
/// using an external utility, to ensure the Certify key is usable. /// using an external utility, to ensure the Certify key is usable.
#[command(name = "openpgp")] #[command(name = "openpgp")]
OpenPGP { OpenPGP(OpenPGP)
/// Default User ID for the certificate, using the OpenPGP User ID format. }
user_id: String,
}, #[derive(Args, Clone, Debug)]
pub struct OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format.
user_id: String,
} }
impl DeriveSubcommands { impl DeriveSubcommands {
fn handle(&self, account: DerivationIndex) -> Result<()> { fn handle(&self, account: DerivationIndex) -> Result<()> {
match self { match self {
DeriveSubcommands::OpenPGP { user_id } => { DeriveSubcommands::OpenPGP(opgp) => opgp.handle(account),
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 path = DerivationPath::default()
.chain_push(chain)
.chain_push(account);
// TODO: should this be customizable?
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let default_userid = UserID::from(user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?; impl OpenPGP {
pub fn handle(&self, account: DerivationIndex) -> Result<()> {
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 path = DerivationPath::default()
.chain_push(chain)
.chain_push(account);
// TODO: should this be customizable?
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let default_userid = UserID::from(self.user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
for packet in cert.into_packets() { let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
packet.serialize(&mut w)?;
}
w.finalize()?; for packet in cert.into_packets() {
} packet.serialize(&mut w)?;
} }
w.finalize()?;
Ok(()) Ok(())
} }
} }

View File

@ -1,26 +1,28 @@
use super::Keyfork; use super::Keyfork;
use clap::{Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use std::{ use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
collections::HashSet,
fs::File,
io::IsTerminal,
path::{Path, PathBuf},
};
use card_backend_pcsc::PcscBackend; use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::KeyType, Card}; use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
use keyfork_derive_openpgp::{ use keyfork_derive_openpgp::{
openpgp::{self, packet::UserID, types::KeyFlags, Cert, serialize::Marshal, armor::{Writer, Kind}}, openpgp::{
self,
armor::{Kind, Writer},
packet::UserID,
serialize::Marshal,
types::KeyFlags,
Cert,
},
XPrv, XPrv,
}; };
use keyfork_derive_util::{DerivationIndex, DerivationPath, VariableLengthSeed}; use keyfork_derive_util::{DerivationIndex, DerivationPath, VariableLengthSeed};
use keyfork_mnemonic_util::Mnemonic;
use keyfork_prompt::{ use keyfork_prompt::{
default_terminal, default_terminal,
validators::{SecurePinValidator, Validator}, validators::{SecurePinValidator, Validator},
DefaultTerminal, Message, PromptHandler, DefaultTerminal, Message, PromptHandler,
}; };
use keyfork_mnemonic_util::Mnemonic;
use keyfork_shard::{openpgp::OpenPGP, Format}; use keyfork_shard::{openpgp::OpenPGP, Format};
@ -109,202 +111,205 @@ fn factory_reset_current_card(
Ok(()) Ok(())
} }
fn generate_shard_secret(
threshold: u8,
max: u8,
keys_per_shard: u8,
output_file: &Option<PathBuf>,
) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mut pm = default_terminal()?;
let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout();
if output_file.is_none() {
assert!(
!stdout.is_terminal(),
"not printing shard to terminal, redirect output"
);
}
let user_pin_validator = SecurePinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = SecurePinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
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 remove all keys and insert key #{} for user #{}",
(i as u16) + 1,
(index as u16) + 1,
)))?;
let card_backend = loop {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
break c;
}
pm.prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter"
.to_string(),
))?;
};
let user_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
let admin_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
factory_reset_current_card(
&mut seen_cards,
user_pin.trim(),
admin_pin.trim(),
&cert,
card_backend,
)?;
}
certs.push(cert);
}
let opgp = OpenPGP::<DefaultTerminal>::new();
if let Some(output_file) = output_file {
let output = File::create(output_file)?;
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
} else {
opgp.shard_and_encrypt(
threshold,
certs.len() as u8,
&seed,
&certs[..],
std::io::stdout(),
)?;
}
Ok(())
}
fn bottoms_up(key_discovery: &Path, threshold: u8, output_shardfile: &Path, output_cert: &Path, user_id: &str,) -> Result<()> {
let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mnemonic = Mnemonic::from_nonstandard_bytes(entropy);
// TODO: make this return const size, since is hash based
let seed = mnemonic.generate_seed(None);
// TODO: should this allow for customizing the account index from 0? Potential for key reuse
// errors.
let path = DerivationPath::default()
.chain_push(DerivationIndex::new(u32::from_be_bytes(*b"\x00pgp"), true)?)
.chain_push(DerivationIndex::new(u32::from_be_bytes(*b"\x00\x00dr"), true)?)
.chain_push(DerivationIndex::new(0, true)?);
let subkeys = [
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = XPrv::new(VariableLengthSeed::new(&seed))
.expect("could not construct master key from seed")
.derive_path(&path)?;
let userid = UserID::from(user_id);
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
let certfile = File::create(output_cert)?;
let mut w = Writer::new(certfile, Kind::PublicKey)?;
cert.serialize(&mut w)?;
w.finalize()?;
let opgp = OpenPGP::<DefaultTerminal>::new();
let certs = OpenPGP::<DefaultTerminal>::discover_certs(key_discovery)?;
let shardfile = File::create(output_shardfile)?;
opgp.shard_and_encrypt(threshold, certs.len() as u8, &entropy, &certs[..], shardfile)?;
Ok(())
}
#[derive(Subcommand, Clone, Debug)] #[derive(Subcommand, Clone, Debug)]
pub enum WizardSubcommands { pub enum WizardSubcommands {
/// Create a 256 bit secret and shard the secret to smart cards. GenerateShardSecret(GenerateShardSecret),
/// BottomsUp(BottomsUp),
/// 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. /// Create a 256 bit secret and shard the secret to smart cards.
#[arg(long)] ///
max: u8, /// 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.
#[derive(Args, Clone, Debug)]
pub struct GenerateShardSecret {
/// The minimum amount of keys required to decrypt the secret.
#[arg(long)]
threshold: u8,
/// The amount of smart cards to provision per-shard. /// The maximum amount of shards.
#[arg(long, default_value = "1")] #[arg(long)]
keys_per_shard: u8, max: u8,
/// The file to write the generated shard file to. /// The amount of smart cards to provision per-shard.
#[arg(long)] #[arg(long, default_value = "1")]
output: Option<PathBuf>, keys_per_shard: u8,
},
/// Create a 256 bit secret and shard the secret to previously known OpenPGP certificates, /// The file to write the generated shard file to.
/// deriving the default OpenPGP certificate for the secret. #[arg(long)]
/// output: Option<PathBuf>,
/// This command was purpose-built for DEFCON and is not intended to be used normally, as it }
/// implies keys used for sharding have been generated by a custom source.
BottomsUp {
/// The location of OpenPGP certificates to use when sharding.
key_discovery: PathBuf,
/// The minimum amount of keys required to decrypt the secret. /// Create a 256 bit secret and shard the secret to previously known OpenPGP certificates,
#[arg(long)] /// deriving the default OpenPGP certificate for the secret.
threshold: u8, ///
/// This command was purpose-built for DEFCON and is not intended to be used normally, as it
/// implies keys used for sharding have been generated by a custom source.
#[derive(Args, Clone, Debug)]
pub struct BottomsUp {
/// The location of OpenPGP certificates to use when sharding.
key_discovery: PathBuf,
/// The file to write the generated shard file to. /// The minimum amount of keys required to decrypt the secret.
#[arg(long)] #[arg(long)]
output_shardfile: PathBuf, threshold: u8,
/// The file to write the generated OpenPGP certificate to. /// The file to write the generated shard file to.
#[arg(long)] #[arg(long)]
output_cert: PathBuf, output_shardfile: PathBuf,
/// The User ID for the generated OpenPGP certificate. /// The file to write the generated OpenPGP certificate to.
#[arg(long, default_value = "Disaster Recovery")] #[arg(long)]
user_id: String, output_cert: PathBuf,
},
/// The User ID for the generated OpenPGP certificate.
#[arg(long, default_value = "Disaster Recovery")]
user_id: String,
} }
impl WizardSubcommands { impl WizardSubcommands {
// dispatch
fn handle(&self) -> Result<()> { fn handle(&self) -> Result<()> {
match self { match self {
WizardSubcommands::GenerateShardSecret { WizardSubcommands::GenerateShardSecret(gss) => gss.handle(),
threshold, WizardSubcommands::BottomsUp(bu) => bu.handle(),
max,
keys_per_shard,
output,
} => generate_shard_secret(*threshold, *max, *keys_per_shard, output),
WizardSubcommands::BottomsUp {
key_discovery,
threshold,
output_shardfile,
output_cert,
user_id,
} => bottoms_up(key_discovery, *threshold, output_shardfile, output_cert, user_id),
} }
} }
} }
impl GenerateShardSecret {
fn handle(&self) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mut pm = default_terminal()?;
let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout();
if self.output.is_none() {
assert!(
!stdout.is_terminal(),
"not printing shard to terminal, redirect output"
);
}
let user_pin_validator = SecurePinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = SecurePinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
for index in 0..self.max {
let cert = derive_key(seed, index)?;
for i in 0..self.keys_per_shard {
pm.prompt_message(Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}",
(i as u16) + 1,
(index as u16) + 1,
)))?;
let card_backend = loop {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
break c;
}
pm.prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter"
.to_string(),
))?;
};
let user_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
let admin_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
factory_reset_current_card(
&mut seen_cards,
user_pin.trim(),
admin_pin.trim(),
&cert,
card_backend,
)?;
}
certs.push(cert);
}
let opgp = OpenPGP::<DefaultTerminal>::new();
if let Some(output_file) = self.output.as_ref() {
let output = File::create(output_file)?;
opgp.shard_and_encrypt(self.threshold, certs.len() as u8, &seed, &certs[..], output)?;
} else {
opgp.shard_and_encrypt(
self.threshold,
certs.len() as u8,
&seed,
&certs[..],
std::io::stdout(),
)?;
}
Ok(())
}
}
impl BottomsUp {
fn handle(&self) -> Result<()> {
let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mnemonic = Mnemonic::from_nonstandard_bytes(entropy);
// TODO: make this return const size, since is hash based
let seed = mnemonic.generate_seed(None);
// TODO: should this allow for customizing the account index from 0? Potential for key reuse
// errors.
let path = DerivationPath::default()
.chain_push(DerivationIndex::new(u32::from_be_bytes(*b"\x00pgp"), true)?)
.chain_push(DerivationIndex::new(
u32::from_be_bytes(*b"\x00\x00dr"),
true,
)?)
.chain_push(DerivationIndex::new(0, true)?);
let subkeys = [
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = XPrv::new(VariableLengthSeed::new(&seed))
.expect("could not construct master key from seed")
.derive_path(&path)?;
let userid = UserID::from(self.user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
let certfile = File::create(&self.output_cert)?;
let mut w = Writer::new(certfile, Kind::PublicKey)?;
cert.serialize(&mut w)?;
w.finalize()?;
let opgp = OpenPGP::<DefaultTerminal>::new();
let certs = OpenPGP::<DefaultTerminal>::discover_certs(&self.key_discovery)?;
let shardfile = File::create(&self.output_shardfile)?;
opgp.shard_and_encrypt(
self.threshold,
certs.len() as u8,
&entropy,
&certs[..],
shardfile,
)?;
Ok(())
}
}
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
pub struct Wizard { pub struct Wizard {
#[command(subcommand)] #[command(subcommand)]