keyfork: the wizard is dead! long live the mnemonic generator!
This commit is contained in:
parent
a1c3d52c14
commit
53665cac2e
|
@ -1,4 +1,4 @@
|
||||||
use super::Keyfork;
|
use super::{Keyfork, create};
|
||||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||||
use std::{fmt::Display, io::Write, path::PathBuf};
|
use std::{fmt::Display, io::Write, path::PathBuf};
|
||||||
|
|
||||||
|
@ -20,11 +20,6 @@ type OptWrite = Option<Box<dyn Write>>;
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
fn create(path: &std::path::Path) -> std::io::Result<std::fs::File> {
|
|
||||||
eprintln!("Writing derived key to: {path}", path=path.display());
|
|
||||||
std::fs::File::create(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Deriver {
|
pub trait Deriver {
|
||||||
type Prv: PrivateKey + Clone;
|
type Prv: PrivateKey + Clone;
|
||||||
const DERIVATION_ALGORITHM: DerivationAlgorithm;
|
const DERIVATION_ALGORITHM: DerivationAlgorithm;
|
||||||
|
@ -54,10 +49,47 @@ pub enum DeriveSubcommands {
|
||||||
Key(Key),
|
Key(Key),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derivation path to use when deriving OpenPGP keys.
|
||||||
|
#[derive(ValueEnum, Clone, Debug, Default)]
|
||||||
|
pub enum Path {
|
||||||
|
/// The default derivation path; no additional index is used.
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
|
||||||
|
/// The Disaster Recovery index.
|
||||||
|
DisasterRecovery,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Path {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Path {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Path::Default => "default",
|
||||||
|
Path::DisasterRecovery => "disaster-recovery",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derivation_path(&self) -> DerivationPath {
|
||||||
|
match self {
|
||||||
|
Self::Default => paths::OPENPGP.clone(),
|
||||||
|
Self::DisasterRecovery => paths::OPENPGP_DISASTER_RECOVERY.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args, Clone, Debug)]
|
#[derive(Args, Clone, Debug)]
|
||||||
pub struct OpenPGP {
|
pub struct OpenPGP {
|
||||||
/// Default User ID for the certificate, using the OpenPGP User ID format.
|
/// Default User ID for the certificate, using the OpenPGP User ID format.
|
||||||
user_id: String,
|
user_id: String,
|
||||||
|
|
||||||
|
/// Derivation path to use when deriving OpenPGP keys.
|
||||||
|
#[arg(long, required = false, default_value = "default")]
|
||||||
|
derivation_path: Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A format for exporting a key.
|
/// A format for exporting a key.
|
||||||
|
@ -173,7 +205,7 @@ impl Deriver for OpenPGP {
|
||||||
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
|
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
|
||||||
|
|
||||||
fn derivation_path(&self) -> DerivationPath {
|
fn derivation_path(&self) -> DerivationPath {
|
||||||
paths::OPENPGP.clone()
|
self.derivation_path.derivation_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
|
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use super::{
|
use super::{
|
||||||
|
create,
|
||||||
derive::{self, Deriver},
|
derive::{self, Deriver},
|
||||||
provision, Keyfork,
|
provision,
|
||||||
|
Keyfork,
|
||||||
};
|
};
|
||||||
use crate::{clap_ext::*, config};
|
use crate::{clap_ext::*, config, openpgp_card::factory_reset_current_card};
|
||||||
|
use card_backend_pcsc::PcscBackend;
|
||||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
@ -17,18 +20,21 @@ use keyfork_derive_openpgp::{
|
||||||
openpgp::{
|
openpgp::{
|
||||||
self,
|
self,
|
||||||
armor::{Kind, Writer},
|
armor::{Kind, Writer},
|
||||||
packet::UserID,
|
packet::{UserID, signature::SignatureBuilder},
|
||||||
policy::StandardPolicy,
|
policy::StandardPolicy,
|
||||||
serialize::{
|
serialize::{
|
||||||
stream::{Encryptor2, LiteralWriter, Message, Recipient},
|
stream::{Encryptor2, LiteralWriter, Message, Recipient},
|
||||||
Serialize,
|
Serialize,
|
||||||
},
|
},
|
||||||
types::KeyFlags,
|
types::{KeyFlags, SignatureType},
|
||||||
},
|
},
|
||||||
XPrv,
|
XPrv,
|
||||||
};
|
};
|
||||||
use keyfork_derive_util::DerivationIndex;
|
use keyfork_derive_util::DerivationIndex;
|
||||||
use keyfork_prompt::default_handler;
|
use keyfork_prompt::{
|
||||||
|
default_handler, prompt_validated_passphrase,
|
||||||
|
validators::{SecurePinValidator, Validator},
|
||||||
|
};
|
||||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||||
|
|
||||||
type StringMap = HashMap<String, String>;
|
type StringMap = HashMap<String, String>;
|
||||||
|
@ -153,6 +159,10 @@ pub enum Error {
|
||||||
/// An error occurred when interacting iwth a file.
|
/// An error occurred when interacting iwth a file.
|
||||||
#[error("Error while performing IO operation on: {1}")]
|
#[error("Error while performing IO operation on: {1}")]
|
||||||
IOContext(#[source] std::io::Error, PathBuf),
|
IOContext(#[source] std::io::Error, PathBuf),
|
||||||
|
|
||||||
|
/// A required option was not provided.
|
||||||
|
#[error("The required option {0} was not provided")]
|
||||||
|
MissingOption(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn context_stub<'a>(path: &'a Path) -> impl Fn(std::io::Error) -> Error + 'a {
|
fn context_stub<'a>(path: &'a Path) -> impl Fn(std::io::Error) -> Error + 'a {
|
||||||
|
@ -241,6 +251,23 @@ pub enum MnemonicSubcommands {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
encrypt_to_self: Option<PathBuf>,
|
encrypt_to_self: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Shard the mnemonic to freshly-generated OpenPGP certificates derived from the mnemonic,
|
||||||
|
/// writing the output to the provided path, and provisioning OpenPGP smartcards with the
|
||||||
|
/// new certificates.
|
||||||
|
///
|
||||||
|
/// The following additional arguments are required:
|
||||||
|
///
|
||||||
|
/// * threshold, m: the minimum amount of shares required to reconstitute the shard.
|
||||||
|
///
|
||||||
|
/// * max, n: the maximum amount of shares.
|
||||||
|
///
|
||||||
|
/// * cards_per_shard: the amount of OpenPGP smartcards to provision per shardholder.
|
||||||
|
///
|
||||||
|
/// * cert_output: the file to write all generated OpenPGP certificates to; if not
|
||||||
|
/// provided, files will be automatically generated for each certificate.
|
||||||
|
#[arg(long)]
|
||||||
|
shard_to_self: Option<ValueWithOptions<PathBuf>>,
|
||||||
|
|
||||||
/// Provision a key derived from the mnemonic to a piece of hardware such as an OpenPGP
|
/// Provision a key derived from the mnemonic to a piece of hardware such as an OpenPGP
|
||||||
/// smartcard. This argument is required when used with `--encrypt-to-self`.
|
/// smartcard. This argument is required when used with `--encrypt-to-self`.
|
||||||
///
|
///
|
||||||
|
@ -506,6 +533,173 @@ fn do_shard_to(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn derive_key(seed: [u8; 64], index: u8) -> Result<openpgp::Cert, Box<dyn std::error::Error>> {
|
||||||
|
let subkeys = vec![
|
||||||
|
KeyFlags::empty().set_certification(),
|
||||||
|
KeyFlags::empty().set_signing(),
|
||||||
|
KeyFlags::empty()
|
||||||
|
.set_transport_encryption()
|
||||||
|
.set_storage_encryption(),
|
||||||
|
KeyFlags::empty().set_authentication(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let subkey = DerivationIndex::new(u32::from(index), true)?;
|
||||||
|
let path = keyfork_derive_path_data::paths::OPENPGP_SHARD.clone().chain_push(subkey);
|
||||||
|
let xprv = XPrv::new(seed)
|
||||||
|
.expect("could not construct master key from seed")
|
||||||
|
.derive_path(&path)?;
|
||||||
|
let userid = UserID::from(format!("Keyfork Shard {index}"));
|
||||||
|
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
|
||||||
|
Ok(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cross_sign_certs(certs: &mut [openpgp::Cert]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let policy = StandardPolicy::new();
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_to_owned)]
|
||||||
|
for signing_cert in certs.to_vec() {
|
||||||
|
let mut certify_key = signing_cert
|
||||||
|
.with_policy(&policy, None)?
|
||||||
|
.keys()
|
||||||
|
.unencrypted_secret()
|
||||||
|
.for_certification()
|
||||||
|
.next()
|
||||||
|
.expect("certify key unusable/not found")
|
||||||
|
.key()
|
||||||
|
.clone()
|
||||||
|
.into_keypair()?;
|
||||||
|
for signable_cert in certs.iter_mut() {
|
||||||
|
let sb = SignatureBuilder::new(SignatureType::GenericCertification);
|
||||||
|
let userid = signable_cert
|
||||||
|
.userids()
|
||||||
|
.next()
|
||||||
|
.expect("a signable user ID is necessary to create web of trust");
|
||||||
|
let signature = sb.sign_userid_binding(
|
||||||
|
&mut certify_key,
|
||||||
|
signable_cert.primary_key().key(),
|
||||||
|
&userid,
|
||||||
|
)?;
|
||||||
|
let changed;
|
||||||
|
(*signable_cert, changed) = signable_cert.clone().insert_packets2(signature)?;
|
||||||
|
assert!(
|
||||||
|
changed,
|
||||||
|
"OpenPGP certificate was unchanged after inserting packets"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn do_shard_to_self(
|
||||||
|
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||||
|
path: &Path,
|
||||||
|
options: &StringMap,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let seed = mnemonic.generate_seed(None);
|
||||||
|
let mut pm = default_handler()?;
|
||||||
|
let mut certs = vec![];
|
||||||
|
let mut seen_cards = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
let threshold: u8 = options
|
||||||
|
.get("threshold")
|
||||||
|
.or(options.get("m"))
|
||||||
|
.ok_or(Error::MissingOption("threshold"))?
|
||||||
|
.parse()?;
|
||||||
|
let max: u8 = options
|
||||||
|
.get("max")
|
||||||
|
.or(options.get("n"))
|
||||||
|
.ok_or(Error::MissingOption("max"))?
|
||||||
|
.parse()?;
|
||||||
|
let cards_per_shard = options
|
||||||
|
.get("cards_per_shard")
|
||||||
|
.as_deref()
|
||||||
|
.map(|cps| u8::from_str(cps))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let 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..cards_per_shard.unwrap_or(1) {
|
||||||
|
pm.prompt_message(keyfork_prompt::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(keyfork_prompt::Message::Text(
|
||||||
|
"No smart card was found. Please plug in a smart card and press enter"
|
||||||
|
.to_string(),
|
||||||
|
))?;
|
||||||
|
};
|
||||||
|
let pin = prompt_validated_passphrase(
|
||||||
|
&mut *pm,
|
||||||
|
"Please enter the new smartcard PIN: ",
|
||||||
|
3,
|
||||||
|
&pin_validator,
|
||||||
|
)?;
|
||||||
|
factory_reset_current_card(
|
||||||
|
&mut |application_identifier| {
|
||||||
|
if seen_cards.contains(&application_identifier) {
|
||||||
|
// we were given a previously-seen card, error
|
||||||
|
// we're gonna panic because this is a significant error
|
||||||
|
panic!("Previously used card {application_identifier} was reused");
|
||||||
|
} else {
|
||||||
|
seen_cards.insert(application_identifier);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pin.trim(),
|
||||||
|
pin.trim(),
|
||||||
|
&cert,
|
||||||
|
&openpgp::policy::NullPolicy::new(),
|
||||||
|
card_backend,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
certs.push(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
cross_sign_certs(&mut certs)?;
|
||||||
|
|
||||||
|
let opgp = OpenPGP;
|
||||||
|
let output = File::create(path)?;
|
||||||
|
opgp.shard_and_encrypt(
|
||||||
|
threshold,
|
||||||
|
certs.len() as u8,
|
||||||
|
mnemonic.as_bytes(),
|
||||||
|
&certs[..],
|
||||||
|
output,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
match options.get("cert_output") {
|
||||||
|
Some(path) => {
|
||||||
|
let cert_file = std::fs::File::create(path)?;
|
||||||
|
let mut writer = Writer::new(cert_file, Kind::PublicKey)?;
|
||||||
|
for cert in &certs {
|
||||||
|
cert.serialize(&mut writer)?;
|
||||||
|
}
|
||||||
|
writer.finalize()?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
for cert in &certs {
|
||||||
|
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
|
||||||
|
let file = create(&path)?;
|
||||||
|
let mut writer = Writer::new(file, Kind::PublicKey)?;
|
||||||
|
cert.serialize(&mut writer)?;
|
||||||
|
writer.finalize()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn do_provision(
|
fn do_provision(
|
||||||
mnemonic: &keyfork_mnemonic::Mnemonic,
|
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||||
provision: &provision::Provision,
|
provision: &provision::Provision,
|
||||||
|
@ -622,20 +816,29 @@ impl MnemonicSubcommands {
|
||||||
shard_to,
|
shard_to,
|
||||||
shard,
|
shard,
|
||||||
encrypt_to_self,
|
encrypt_to_self,
|
||||||
|
shard_to_self,
|
||||||
provision,
|
provision,
|
||||||
provision_count,
|
provision_count,
|
||||||
provision_config,
|
provision_config,
|
||||||
} => {
|
} => {
|
||||||
// NOTE: We should never have a case where there's Some() of empty vec, but
|
// NOTE: We should never have a case where there's Some() of empty vec, but
|
||||||
// we will make sure to check it just in case.
|
// we will make sure to check it just in case.
|
||||||
|
//
|
||||||
|
// We do not print the mnemonic if we are:
|
||||||
|
// * Encrypting to an existing, usable key
|
||||||
|
// * Encrypting to a newly provisioned key
|
||||||
|
// * Sharding to an existing Shardfile with usable keys
|
||||||
|
// * Sharding to existing, usable keys
|
||||||
|
// * Sharding to newly provisioned keys
|
||||||
let mut will_print_mnemonic =
|
let mut will_print_mnemonic =
|
||||||
encrypt_to.is_none() || encrypt_to.as_ref().is_some_and(|e| e.is_empty());
|
encrypt_to.is_none() || encrypt_to.as_ref().is_some_and(|e| e.is_empty());
|
||||||
|
will_print_mnemonic = will_print_mnemonic
|
||||||
|
&& (encrypt_to_self.as_ref().is_none() || provision.as_ref().is_none());
|
||||||
will_print_mnemonic = will_print_mnemonic && shard_to.is_none()
|
will_print_mnemonic = will_print_mnemonic && shard_to.is_none()
|
||||||
|| shard_to.as_ref().is_some_and(|s| s.is_empty());
|
|| shard_to.as_ref().is_some_and(|s| s.is_empty());
|
||||||
will_print_mnemonic = will_print_mnemonic && shard.is_none()
|
will_print_mnemonic = will_print_mnemonic && shard.is_none()
|
||||||
|| shard.as_ref().is_some_and(|s| s.is_empty());
|
|| shard.as_ref().is_some_and(|s| s.is_empty());
|
||||||
will_print_mnemonic = will_print_mnemonic
|
will_print_mnemonic = will_print_mnemonic && shard_to_self.is_none();
|
||||||
&& (encrypt_to_self.as_ref().is_none() || provision.as_ref().is_none());
|
|
||||||
|
|
||||||
let mnemonic = source.handle(size)?;
|
let mnemonic = source.handle(size)?;
|
||||||
|
|
||||||
|
@ -684,6 +887,10 @@ impl MnemonicSubcommands {
|
||||||
do_encrypt_to_self(&mnemonic, &encrypt_to_self, &indices)?;
|
do_encrypt_to_self(&mnemonic, &encrypt_to_self, &indices)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(shard_to_self) = shard_to_self {
|
||||||
|
do_shard_to_self(&mnemonic, &shard_to_self.inner, &shard_to_self.values)?;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(provisioner) = provision {
|
if let Some(provisioner) = provision {
|
||||||
// determine if we should write to standard output based on whether we have a
|
// determine if we should write to standard output based on whether we have a
|
||||||
// matching pair of provisioner and public derivation output.
|
// matching pair of provisioner and public derivation output.
|
||||||
|
|
|
@ -5,7 +5,11 @@ mod mnemonic;
|
||||||
mod provision;
|
mod provision;
|
||||||
mod recover;
|
mod recover;
|
||||||
mod shard;
|
mod shard;
|
||||||
mod wizard;
|
|
||||||
|
pub fn create(path: &std::path::Path) -> std::io::Result<std::fs::File> {
|
||||||
|
eprintln!("Writing derived key to: {path}", path=path.display());
|
||||||
|
std::fs::File::create(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// The Kitchen Sink of Entropy.
|
/// The Kitchen Sink of Entropy.
|
||||||
#[derive(Parser, Clone, Debug)]
|
#[derive(Parser, Clone, Debug)]
|
||||||
|
@ -57,9 +61,6 @@ pub enum KeyforkCommands {
|
||||||
/// leaked by any individual deriver.
|
/// leaked by any individual deriver.
|
||||||
Recover(recover::Recover),
|
Recover(recover::Recover),
|
||||||
|
|
||||||
/// Utilities to automatically manage the setup of Keyfork.
|
|
||||||
Wizard(wizard::Wizard),
|
|
||||||
|
|
||||||
/// Print an autocompletion file to standard output.
|
/// Print an autocompletion file to standard output.
|
||||||
///
|
///
|
||||||
/// Keyfork does not manage the installation of completion files. Consult the documentation for
|
/// Keyfork does not manage the installation of completion files. Consult the documentation for
|
||||||
|
@ -90,9 +91,6 @@ impl KeyforkCommands {
|
||||||
KeyforkCommands::Recover(r) => {
|
KeyforkCommands::Recover(r) => {
|
||||||
r.handle(keyfork)?;
|
r.handle(keyfork)?;
|
||||||
}
|
}
|
||||||
KeyforkCommands::Wizard(w) => {
|
|
||||||
w.handle(keyfork)?;
|
|
||||||
}
|
|
||||||
#[cfg(feature = "completion")]
|
#[cfg(feature = "completion")]
|
||||||
KeyforkCommands::Completion { shell } => {
|
KeyforkCommands::Completion { shell } => {
|
||||||
let mut command = Keyfork::command();
|
let mut command = Keyfork::command();
|
||||||
|
|
|
@ -59,7 +59,6 @@ impl Provisioner {
|
||||||
.chain_push(account_index);
|
.chain_push(account_index);
|
||||||
let mut client = keyforkd_client::Client::discover_socket()?;
|
let mut client = keyforkd_client::Client::discover_socket()?;
|
||||||
let xprv: XPrv = client.request_xprv(&path)?;
|
let xprv: XPrv = client.request_xprv(&path)?;
|
||||||
panic!();
|
|
||||||
s.provision(xprv, provisioner)
|
s.provision(xprv, provisioner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,335 +0,0 @@
|
||||||
use super::Keyfork;
|
|
||||||
use crate::openpgp_card::factory_reset_current_card;
|
|
||||||
use clap::{Args, Parser, Subcommand};
|
|
||||||
use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
|
|
||||||
|
|
||||||
use card_backend_pcsc::PcscBackend;
|
|
||||||
|
|
||||||
use keyfork_derive_openpgp::{
|
|
||||||
openpgp::{
|
|
||||||
self,
|
|
||||||
armor::{Kind, Writer},
|
|
||||||
packet::{signature::SignatureBuilder, UserID},
|
|
||||||
policy::StandardPolicy,
|
|
||||||
serialize::Marshal,
|
|
||||||
types::{KeyFlags, SignatureType},
|
|
||||||
Cert,
|
|
||||||
},
|
|
||||||
XPrv,
|
|
||||||
};
|
|
||||||
use keyfork_derive_path_data::paths;
|
|
||||||
use keyfork_derive_util::DerivationIndex;
|
|
||||||
use keyfork_mnemonic::Mnemonic;
|
|
||||||
use keyfork_prompt::{
|
|
||||||
default_handler, prompt_validated_passphrase,
|
|
||||||
validators::{SecurePinValidator, Validator},
|
|
||||||
Message,
|
|
||||||
};
|
|
||||||
|
|
||||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
#[error("Invalid PIN length: {0}")]
|
|
||||||
pub struct PinLength(usize);
|
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
|
||||||
|
|
||||||
fn derive_key(seed: [u8; 64], 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 subkey = DerivationIndex::new(u32::from(index), true)?;
|
|
||||||
let path = paths::OPENPGP_SHARD.clone().chain_push(subkey);
|
|
||||||
let xprv = XPrv::new(seed)
|
|
||||||
.expect("could not construct master key from seed")
|
|
||||||
.derive_path(&path)?;
|
|
||||||
let userid = UserID::from(format!("Keyfork Shard {index}"));
|
|
||||||
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
|
|
||||||
Ok(cert)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
|
||||||
pub enum WizardSubcommands {
|
|
||||||
GenerateShardSecret(GenerateShardSecret),
|
|
||||||
BottomsUp(BottomsUp),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
#[derive(Args, Clone, Debug)]
|
|
||||||
pub struct 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,
|
|
||||||
|
|
||||||
/// The file to write the generated shard file to.
|
|
||||||
#[arg(long)]
|
|
||||||
output: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// The file to write generated certificates to.
|
|
||||||
#[arg(long)]
|
|
||||||
cert_output: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a 256 bit secret and shard the secret to previously known OpenPGP certificates,
|
|
||||||
/// deriving the default OpenPGP certificate for the secret.
|
|
||||||
///
|
|
||||||
/// 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 minimum amount of keys required to decrypt the secret.
|
|
||||||
#[arg(long)]
|
|
||||||
threshold: u8,
|
|
||||||
|
|
||||||
/// The file to write the generated shard file to.
|
|
||||||
#[arg(long)]
|
|
||||||
output_shardfile: PathBuf,
|
|
||||||
|
|
||||||
/// The file to write the generated OpenPGP certificate to.
|
|
||||||
#[arg(long)]
|
|
||||||
output_cert: PathBuf,
|
|
||||||
|
|
||||||
/// The User ID for the generated OpenPGP certificate.
|
|
||||||
#[arg(long, default_value = "Disaster Recovery")]
|
|
||||||
user_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WizardSubcommands {
|
|
||||||
// dispatch
|
|
||||||
fn handle(&self) -> Result<()> {
|
|
||||||
match self {
|
|
||||||
WizardSubcommands::GenerateShardSecret(gss) => gss.handle(),
|
|
||||||
WizardSubcommands::BottomsUp(bu) => bu.handle(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cross_sign_certs(certs: &mut [Cert]) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let policy = StandardPolicy::new();
|
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_to_owned)]
|
|
||||||
for signing_cert in certs.to_vec() {
|
|
||||||
let mut certify_key = signing_cert
|
|
||||||
.with_policy(&policy, None)?
|
|
||||||
.keys()
|
|
||||||
.unencrypted_secret()
|
|
||||||
.for_certification()
|
|
||||||
.next()
|
|
||||||
.expect("certify key unusable/not found")
|
|
||||||
.key()
|
|
||||||
.clone()
|
|
||||||
.into_keypair()?;
|
|
||||||
for signable_cert in certs.iter_mut() {
|
|
||||||
let sb = SignatureBuilder::new(SignatureType::GenericCertification);
|
|
||||||
let userid = signable_cert
|
|
||||||
.userids()
|
|
||||||
.next()
|
|
||||||
.expect("a signable user ID is necessary to create web of trust");
|
|
||||||
let signature = sb.sign_userid_binding(
|
|
||||||
&mut certify_key,
|
|
||||||
signable_cert.primary_key().key(),
|
|
||||||
&userid,
|
|
||||||
)?;
|
|
||||||
let changed;
|
|
||||||
(*signable_cert, changed) = signable_cert.clone().insert_packets2(signature)?;
|
|
||||||
assert!(
|
|
||||||
changed,
|
|
||||||
"OpenPGP certificate was unchanged after inserting packets"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GenerateShardSecret {
|
|
||||||
fn handle(&self) -> Result<()> {
|
|
||||||
let root_entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
|
|
||||||
let mnemonic = Mnemonic::from_array(root_entropy);
|
|
||||||
let seed = mnemonic.generate_seed(None);
|
|
||||||
let mut pm = default_handler()?;
|
|
||||||
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 = prompt_validated_passphrase(
|
|
||||||
&mut *pm,
|
|
||||||
"Please enter the new smartcard User PIN: ",
|
|
||||||
3,
|
|
||||||
&user_pin_validator,
|
|
||||||
)?;
|
|
||||||
let admin_pin = prompt_validated_passphrase(
|
|
||||||
&mut *pm,
|
|
||||||
"Please enter the new smartcard Admin PIN: ",
|
|
||||||
3,
|
|
||||||
&admin_pin_validator,
|
|
||||||
)?;
|
|
||||||
factory_reset_current_card(
|
|
||||||
&mut |application_identifier| {
|
|
||||||
if seen_cards.contains(&application_identifier) {
|
|
||||||
// we were given the same card, error
|
|
||||||
// we're gonna panic because this is a significant error
|
|
||||||
panic!("Previously used card {application_identifier} was reused");
|
|
||||||
} else {
|
|
||||||
seen_cards.insert(application_identifier);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
user_pin.trim(),
|
|
||||||
admin_pin.trim(),
|
|
||||||
&cert,
|
|
||||||
&openpgp::policy::NullPolicy::new(),
|
|
||||||
card_backend,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
certs.push(cert);
|
|
||||||
}
|
|
||||||
|
|
||||||
cross_sign_certs(&mut certs)?;
|
|
||||||
|
|
||||||
let opgp = OpenPGP;
|
|
||||||
|
|
||||||
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,
|
|
||||||
mnemonic.as_bytes(),
|
|
||||||
&certs[..],
|
|
||||||
output,
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
opgp.shard_and_encrypt(
|
|
||||||
self.threshold,
|
|
||||||
certs.len() as u8,
|
|
||||||
mnemonic.as_bytes(),
|
|
||||||
&certs[..],
|
|
||||||
std::io::stdout(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(cert_output_file) = self.cert_output.as_ref() {
|
|
||||||
let output = File::create(cert_output_file)?;
|
|
||||||
let mut writer = Writer::new(output, Kind::PublicKey)?;
|
|
||||||
for cert in certs {
|
|
||||||
cert.serialize(&mut writer)?;
|
|
||||||
}
|
|
||||||
writer.finalize()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BottomsUp {
|
|
||||||
fn handle(&self) -> Result<()> {
|
|
||||||
let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
|
|
||||||
let mnemonic = Mnemonic::from_array(entropy);
|
|
||||||
let seed = mnemonic.generate_seed(None);
|
|
||||||
|
|
||||||
// TODO: should this allow for customizing the account index from 0? Potential for key reuse
|
|
||||||
// errors.
|
|
||||||
let path = paths::OPENPGP_DISASTER_RECOVERY
|
|
||||||
.clone()
|
|
||||||
.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(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;
|
|
||||||
let certs = OpenPGP::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)]
|
|
||||||
pub struct Wizard {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: WizardSubcommands,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Wizard {
|
|
||||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
|
||||||
self.command.handle()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue