superpower `keyfork mnemonic generate`
This commit is contained in:
parent
8756c3d233
commit
c232828290
|
@ -0,0 +1,60 @@
|
|||
//! Extensions to clap.
|
||||
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
/// A helper struct for clap arguments that can contain additional arguments. For example:
|
||||
/// `keyfork mnemonic generate --encrypt-to cert.asc,output=encrypted.asc`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ValueWithOptions<T: FromStr>
|
||||
where
|
||||
T::Err: std::error::Error,
|
||||
{
|
||||
/// A mapping between keys and values.
|
||||
pub values: HashMap<String, String>,
|
||||
|
||||
/// The first variable for the argument, such as a [`PathBuf`].
|
||||
pub inner: T,
|
||||
}
|
||||
|
||||
/// An error that occurred while parsing a base value or its
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ValueParseError {
|
||||
/// No value was given; the required type could not be parsed.
|
||||
#[error("No value was given")]
|
||||
NoValue,
|
||||
|
||||
/// The first value could not properly be parsed.
|
||||
#[error("Could not parse first value: {0}")]
|
||||
BadParse(String),
|
||||
|
||||
/// Additional values were added, but not in a key=value format.
|
||||
#[error("A key-value pair was not given")]
|
||||
BadKeyValue,
|
||||
}
|
||||
|
||||
impl<T: std::str::FromStr> FromStr for ValueWithOptions<T>
|
||||
where
|
||||
<T as FromStr>::Err: std::error::Error,
|
||||
{
|
||||
type Err = ValueParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut values = s.split(',');
|
||||
let first = values.next().ok_or(ValueParseError::NoValue)?;
|
||||
let mut others = HashMap::new();
|
||||
for value in values {
|
||||
let [lhs, rhs] = value
|
||||
.splitn(2, '=')
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.map_err(|_| ValueParseError::BadKeyValue)?;
|
||||
others.insert(lhs.to_string(), rhs.to_string());
|
||||
}
|
||||
Ok(Self {
|
||||
inner: first
|
||||
.parse()
|
||||
.map_err(|e: <T as FromStr>::Err| ValueParseError::BadParse(e.to_string()))?,
|
||||
values: others,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,6 +1,34 @@
|
|||
use super::provision;
|
||||
use super::Keyfork;
|
||||
use crate::{clap_ext::*, config};
|
||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||
use std::fmt::Display;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
fs::File,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use keyfork_derive_openpgp::{
|
||||
openpgp::{
|
||||
self,
|
||||
armor::{Kind, Writer},
|
||||
packet::UserID,
|
||||
policy::StandardPolicy,
|
||||
serialize::{
|
||||
stream::{Encryptor2, LiteralWriter, Message, Recipient},
|
||||
Serialize,
|
||||
},
|
||||
types::KeyFlags,
|
||||
},
|
||||
XPrv,
|
||||
};
|
||||
use keyfork_prompt::default_handler;
|
||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||
|
||||
type StringMap = HashMap<String, String>;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum SeedSize {
|
||||
|
@ -96,21 +124,22 @@ impl std::str::FromStr for MnemonicSeedSource {
|
|||
}
|
||||
|
||||
impl MnemonicSeedSource {
|
||||
pub fn handle(&self, size: &SeedSize) -> Result<String, Box<dyn std::error::Error>> {
|
||||
pub fn handle(
|
||||
&self,
|
||||
size: &SeedSize,
|
||||
) -> Result<keyfork_mnemonic::Mnemonic, Box<dyn std::error::Error>> {
|
||||
let size = match size {
|
||||
SeedSize::Bits128 => 128,
|
||||
SeedSize::Bits256 => 256,
|
||||
};
|
||||
let seed = match self {
|
||||
MnemonicSeedSource::System => {
|
||||
keyfork_entropy::generate_entropy_of_size(size / 8)?
|
||||
}
|
||||
MnemonicSeedSource::System => keyfork_entropy::generate_entropy_of_size(size / 8)?,
|
||||
MnemonicSeedSource::Playing => todo!(),
|
||||
MnemonicSeedSource::Tarot => todo!(),
|
||||
MnemonicSeedSource::Dice => todo!(),
|
||||
};
|
||||
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?;
|
||||
Ok(mnemonic.to_string())
|
||||
Ok(mnemonic)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,6 +153,10 @@ pub enum MnemonicSubcommands {
|
|||
/// method of generating a seed using system entropy, as well as various forms of loading
|
||||
/// physicalized entropy into a mnemonic. The mnemonic should be stored in a safe location
|
||||
/// (such as a Trezor "recovery seed card") and never persisted digitally.
|
||||
///
|
||||
/// When using the `--shard`, `--shard-to`, `--encrypt-to`, and `--encrypt-to-self` +
|
||||
/// `--provision` arguments, the mnemonic is _not_ sent to output. The data for the mnemonic is
|
||||
/// then either split using Keyfork Shard or encrypted using OpenPGP.
|
||||
Generate {
|
||||
/// The source from where a seed is created.
|
||||
#[arg(long, value_enum, default_value_t = Default::default())]
|
||||
|
@ -132,17 +165,423 @@ pub enum MnemonicSubcommands {
|
|||
/// The size of the mnemonic, in bits.
|
||||
#[arg(long, default_value_t = Default::default())]
|
||||
size: SeedSize,
|
||||
|
||||
/// Encrypt the mnemonic to an OpenPGP certificate in the provided path.
|
||||
///
|
||||
/// When given arguments in the format `--encrypt-to input.asc,output=output.asc`, the
|
||||
/// output of the encryption will be written to `output.asc`. Otherwise, the default
|
||||
/// behavior is to write the output to `input.enc.asc`. If the output file already exists,
|
||||
/// it will not be overwritten, and the command will exit unsuccessfully.
|
||||
#[arg(long)]
|
||||
encrypt_to: Option<Vec<ValueWithOptions<PathBuf>>>,
|
||||
|
||||
/// Shard the mnemonic to the certificates in the given Shardfile. Requires a decrypt
|
||||
/// operation on the Shardfile to access the metadata and certificates.
|
||||
///
|
||||
/// When given arguments in the format `--encrypt-to input.asc,output=output.asc`, the
|
||||
/// output of the encryption will be written to `output.asc`. Otherwise, the default
|
||||
/// behavior is to write the output to `input.new.asc`. If the output file already exists,
|
||||
/// it will not be overwritten, and the command will exit unsuccessfully.
|
||||
#[arg(long)]
|
||||
shard_to: Option<Vec<ValueWithOptions<PathBuf>>>,
|
||||
|
||||
/// Shard the mnemonic to the provided certificates.
|
||||
///
|
||||
/// The following additional arguments are available:
|
||||
///
|
||||
/// * threshold, m: the minimum amount of shares required to reconstitute the shard. By
|
||||
/// default, this is the amount of certificates provided.
|
||||
///
|
||||
/// * max, n: the maximum amount of shares. When provided, this is used to ensure the
|
||||
/// certificate count is correct. This is required when using `threshold` or `m`.
|
||||
///
|
||||
/// * output: the file to write the generated Shardfile to. By default, assuming the
|
||||
/// certificate input is `input.asc`, the generated Shardfile would be written to
|
||||
/// `input.shard.asc`.
|
||||
#[arg(long)]
|
||||
shard: Option<Vec<ValueWithOptions<PathBuf>>>,
|
||||
|
||||
/// Encrypt the mnemonic to an OpenPGP certificate derived from the mnemonic, writing the
|
||||
/// output to the provided path. This command must be run in combination with
|
||||
/// `--provision openpgp-card` or another relevant provisioner, to ensure the newly
|
||||
/// generated mnemonic would be decryptable by some form of provisioned hardware.
|
||||
///
|
||||
/// When given arguments in the format `--encrypt-to-self output.asc,output=encrypted.asc`,
|
||||
/// the output of the OpenPGP certificate will be written to `output.asc`, while the output
|
||||
/// of the encryption will be written to `encrypted.asc`. Otherwise, the
|
||||
/// default behavior is to write the encrypted mnemonic to `output.enc.asc`. If either
|
||||
/// output file already exists, it will not be overwritten, and the command will exit
|
||||
/// unsuccessfully.
|
||||
///
|
||||
/// Additionally, when given the `account=` option (which must match the `account=` option
|
||||
/// of the relevant provisioner), the given account will be used instead of the default
|
||||
/// account of 0.
|
||||
///
|
||||
/// Because a new OpenPGP key needs to be created, a User ID can also be supplied, using
|
||||
/// the option `userid=<your User ID>`. It can contain any characters that are not a comma.
|
||||
#[arg(long)]
|
||||
encrypt_to_self: Option<ValueWithOptions<PathBuf>>,
|
||||
|
||||
/// 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`.
|
||||
///
|
||||
/// Additional arguments, such as the amount of hardware to provision and the
|
||||
/// account to use when deriving, can be specified by using (for example)
|
||||
/// `--provision openpgp-card,count=2,account=1`.
|
||||
#[arg(long)]
|
||||
provision: Option<ValueWithOptions<provision::Provisioner>>,
|
||||
},
|
||||
}
|
||||
|
||||
// NOTE: This function defaults to `.asc` in the event no extension is found.
|
||||
// This is specific to OpenPGP. If you want to use this function elsewhere (why?),
|
||||
// be sure to use a relevant extension for your context.
|
||||
fn determine_valid_output_path<T: AsRef<Path>>(
|
||||
path: &Path,
|
||||
mid_ext: &str,
|
||||
optional_path: Option<T>,
|
||||
) -> PathBuf {
|
||||
match optional_path {
|
||||
Some(p) => p.as_ref().to_path_buf(),
|
||||
None => {
|
||||
let extension = match path.extension() {
|
||||
Some(ext) => format!("{mid_ext}.{ext}", ext = ext.to_string_lossy()),
|
||||
None => format!("{mid_ext}.asc"),
|
||||
};
|
||||
path.with_extension(extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_extension_armored(path: &Path) -> bool {
|
||||
match path.extension().and_then(|s| s.to_str()) {
|
||||
Some("pgp") | Some("gpg") => false,
|
||||
Some("asc") => true,
|
||||
_ => {
|
||||
eprintln!("unable to determine whether to armor file: {path:?}");
|
||||
eprintln!("use .gpg, .pgp, or .asc extension, or `armor=true`");
|
||||
eprintln!("defaulting to armored");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn do_encrypt_to(
|
||||
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||
path: &Path,
|
||||
options: &StringMap,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let policy = StandardPolicy::new();
|
||||
|
||||
let output_file = determine_valid_output_path(path, "enc", options.get("output"));
|
||||
|
||||
let is_armored =
|
||||
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
|
||||
|
||||
let certs = OpenPGP::discover_certs(path)?;
|
||||
let valid_certs = certs
|
||||
.iter()
|
||||
.map(|c| c.with_policy(&policy, None))
|
||||
.collect::<openpgp::Result<Vec<_>>>()?;
|
||||
let recipients = valid_certs.iter().flat_map(|valid_cert| {
|
||||
let keys = valid_cert.keys().alive().for_storage_encryption();
|
||||
keys.map(|key| Recipient::new(key.keyid(), key.key()))
|
||||
});
|
||||
|
||||
let mut output = vec![];
|
||||
let message = Message::new(&mut output);
|
||||
let encrypted_message = Encryptor2::for_recipients(message, recipients).build()?;
|
||||
let mut literal_message = LiteralWriter::new(encrypted_message).build()?;
|
||||
literal_message.write_all(mnemonic.to_string().as_bytes())?;
|
||||
literal_message.write_all(b"\n")?;
|
||||
literal_message.finalize()?;
|
||||
|
||||
let mut file = File::create_new(&output_file)?;
|
||||
if is_armored {
|
||||
let mut writer = Writer::new(file, Kind::Message)?;
|
||||
writer.write_all(&output)?;
|
||||
writer.finalize()?;
|
||||
} else {
|
||||
file.write_all(&output)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_encrypt_to_self(
|
||||
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||
path: &Path,
|
||||
options: &StringMap,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let output_file = determine_valid_output_path(path, "enc", options.get("output"));
|
||||
|
||||
let is_armored =
|
||||
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
|
||||
|
||||
let account = options
|
||||
.get("account")
|
||||
.map(|account| u32::from_str(account))
|
||||
.transpose()?
|
||||
.unwrap_or(0);
|
||||
let account_index = keyfork_derive_util::DerivationIndex::new(account, true)?;
|
||||
|
||||
let userid = options
|
||||
.get("userid")
|
||||
.map(|userid| UserID::from(userid.as_str()));
|
||||
|
||||
let subkeys = [
|
||||
KeyFlags::empty().set_certification(),
|
||||
KeyFlags::empty().set_signing(),
|
||||
KeyFlags::empty()
|
||||
.set_transport_encryption()
|
||||
.set_storage_encryption(),
|
||||
KeyFlags::empty().set_authentication(),
|
||||
];
|
||||
|
||||
let seed = mnemonic.generate_seed(None);
|
||||
let xprv = XPrv::new(seed)?;
|
||||
let derivation_path = keyfork_derive_path_data::paths::OPENPGP
|
||||
.clone()
|
||||
.chain_push(account_index);
|
||||
|
||||
let cert = keyfork_derive_openpgp::derive(
|
||||
xprv.derive_path(&derivation_path)?,
|
||||
&subkeys,
|
||||
&userid.unwrap_or(UserID::from("Keyfork-Generated Key")),
|
||||
)?;
|
||||
|
||||
let mut file = File::create_new(path)?;
|
||||
if is_armored {
|
||||
let mut writer = Writer::new(file, Kind::PublicKey)?;
|
||||
cert.serialize(&mut writer)?;
|
||||
writer.finalize()?;
|
||||
} else {
|
||||
cert.serialize(&mut file)?;
|
||||
}
|
||||
|
||||
// a sneaky bit of DRY
|
||||
do_encrypt_to(
|
||||
mnemonic,
|
||||
path,
|
||||
&StringMap::from([(
|
||||
String::from("output"),
|
||||
output_file.to_string_lossy().to_string(),
|
||||
)]),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Either the threshold(m) or the max(n) values are missing")]
|
||||
struct MissingThresholdOrMax;
|
||||
|
||||
fn do_shard(
|
||||
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||
path: &Path,
|
||||
options: &StringMap,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let output_file = determine_valid_output_path(path, "shard", options.get("output"));
|
||||
|
||||
let is_armored =
|
||||
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
|
||||
|
||||
let threshold = options
|
||||
.get("threshold")
|
||||
.or_else(|| options.get("m"))
|
||||
.map(|s| u8::from_str(s))
|
||||
.transpose()?;
|
||||
|
||||
let max = options
|
||||
.get("max")
|
||||
.or_else(|| options.get("n"))
|
||||
.map(|s| u8::from_str(s))
|
||||
.transpose()?;
|
||||
|
||||
let certs = OpenPGP::discover_certs(path)?;
|
||||
|
||||
// if neither are set: false
|
||||
// if both are set: false
|
||||
// if only one is set: true
|
||||
|
||||
if threshold.is_some() ^ max.is_some() {
|
||||
return Err(MissingThresholdOrMax)?;
|
||||
}
|
||||
|
||||
let (threshold, max) = match threshold.zip(max) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
let len = u8::try_from(certs.len())?;
|
||||
(len, len)
|
||||
}
|
||||
};
|
||||
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||
|
||||
let mut output = vec![];
|
||||
openpgp.shard_and_encrypt(threshold, max, mnemonic.as_bytes(), &certs[..], &mut output)?;
|
||||
|
||||
let mut file = File::create_new(&output_file)?;
|
||||
if is_armored {
|
||||
file.write_all(&output)?;
|
||||
} else {
|
||||
todo!("keyfork does not handle binary shardfiles");
|
||||
/*
|
||||
* NOTE: this code works, but can't be recombined by Keyfork.
|
||||
* therefore, we'll error, before someone tries to use it.
|
||||
let mut dearmor = Reader::from_bytes(&output, ReaderMode::Tolerant(None));
|
||||
std::io::copy(&mut dearmor, &mut file)?;
|
||||
*/
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_shard_to(
|
||||
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||
path: &Path,
|
||||
options: &StringMap,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let output_file = determine_valid_output_path(path, "new", options.get("output"));
|
||||
|
||||
let is_armored =
|
||||
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
|
||||
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||
let prompt = default_handler()?;
|
||||
|
||||
let input = File::open(path)?;
|
||||
let (threshold, certs) = openpgp.decrypt_metadata_from_file(
|
||||
Some(&[][..]), // the things i must do to avoid qualifying types.
|
||||
input,
|
||||
prompt,
|
||||
)?;
|
||||
|
||||
let mut output = vec![];
|
||||
openpgp.shard_and_encrypt(
|
||||
threshold,
|
||||
u8::try_from(certs.len())?,
|
||||
mnemonic.as_bytes(),
|
||||
&certs[..],
|
||||
&mut output,
|
||||
)?;
|
||||
|
||||
let mut file = File::create_new(&output_file)?;
|
||||
if is_armored {
|
||||
file.write_all(&output)?;
|
||||
} else {
|
||||
todo!("keyfork does not handle binary shardfiles");
|
||||
/*
|
||||
* NOTE: this code works, but can't be recombined by Keyfork.
|
||||
* therefore, we'll error, before someone tries to use it.
|
||||
let mut dearmor = Reader::from_bytes(&output, ReaderMode::Tolerant(None));
|
||||
std::io::copy(&mut dearmor, &mut file)?;
|
||||
*/
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("missing key: {0}")]
|
||||
struct MissingKey(&'static str);
|
||||
|
||||
fn do_provision(
|
||||
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||
provisioner: &provision::Provisioner,
|
||||
options: &StringMap,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut options = options.clone();
|
||||
let account = options
|
||||
.remove("account")
|
||||
.map(|account| u32::from_str(&account))
|
||||
.transpose()?
|
||||
.unwrap_or(0);
|
||||
let identifier = options
|
||||
.remove("identifier")
|
||||
.ok_or(MissingKey("identifier"))?
|
||||
.split(',')
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>();
|
||||
let count = options
|
||||
.remove("count")
|
||||
.map(|count| usize::from_str(&count))
|
||||
.transpose()?
|
||||
.unwrap_or(identifier.len());
|
||||
|
||||
for (_, identifier) in (0..count).zip(identifier.into_iter()) {
|
||||
let provisioner_config = config::Provisioner {
|
||||
account,
|
||||
identifier,
|
||||
metadata: Some(options.clone()),
|
||||
};
|
||||
|
||||
provisioner.provision_with_mnemonic(mnemonic, provisioner_config.clone())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl MnemonicSubcommands {
|
||||
pub fn handle(
|
||||
&self,
|
||||
_m: &Mnemonic,
|
||||
_keyfork: &Keyfork,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
MnemonicSubcommands::Generate { source, size } => source.handle(size),
|
||||
MnemonicSubcommands::Generate {
|
||||
source,
|
||||
size,
|
||||
encrypt_to,
|
||||
shard_to,
|
||||
shard,
|
||||
encrypt_to_self,
|
||||
provision,
|
||||
} => {
|
||||
// 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.
|
||||
let mut will_print_mnemonic =
|
||||
encrypt_to.is_none() || encrypt_to.as_ref().is_some_and(|e| e.is_empty());
|
||||
will_print_mnemonic = will_print_mnemonic && shard_to.is_none()
|
||||
|| shard_to.as_ref().is_some_and(|s| s.is_empty());
|
||||
will_print_mnemonic = will_print_mnemonic && shard.is_none()
|
||||
|| shard.as_ref().is_some_and(|s| s.is_empty());
|
||||
will_print_mnemonic = will_print_mnemonic
|
||||
&& (encrypt_to_self.as_ref().is_none() || provision.as_ref().is_none());
|
||||
|
||||
let mnemonic = source.handle(size)?;
|
||||
|
||||
if let Some(encrypt_to) = encrypt_to {
|
||||
for entry in encrypt_to {
|
||||
do_encrypt_to(&mnemonic, &entry.inner, &entry.values)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(encrypt_to_self) = encrypt_to_self {
|
||||
do_encrypt_to_self(&mnemonic, &encrypt_to_self.inner, &encrypt_to_self.values)?;
|
||||
}
|
||||
|
||||
if let Some(provisioner) = provision {
|
||||
do_provision(&mnemonic, &provisioner.inner, &provisioner.values)?;
|
||||
}
|
||||
|
||||
if let Some(shard_to) = shard_to {
|
||||
for entry in shard_to {
|
||||
do_shard_to(&mnemonic, &entry.inner, &entry.values)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(shard) = shard {
|
||||
for entry in shard {
|
||||
do_shard(&mnemonic, &entry.inner, &entry.values)?;
|
||||
}
|
||||
}
|
||||
|
||||
if will_print_mnemonic {
|
||||
println!("{}", mnemonic);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,8 +79,7 @@ impl KeyforkCommands {
|
|||
d.handle(keyfork)?;
|
||||
}
|
||||
KeyforkCommands::Mnemonic(m) => {
|
||||
let response = m.command.handle(m, keyfork)?;
|
||||
println!("{response}");
|
||||
m.command.handle(m, keyfork)?;
|
||||
}
|
||||
KeyforkCommands::Shard(s) => {
|
||||
s.command.handle(s, keyfork)?;
|
||||
|
|
|
@ -3,9 +3,15 @@ use crate::config;
|
|||
|
||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||
|
||||
use keyfork_derive_util::{DerivationIndex, ExtendedPrivateKey};
|
||||
|
||||
mod openpgp;
|
||||
|
||||
type Identifier = (String, Option<String>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Provisioner {
|
||||
OpenPGPCard(OpenPGPCard),
|
||||
OpenPGPCard(openpgp::OpenPGPCard),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Provisioner {
|
||||
|
@ -17,25 +23,52 @@ impl std::fmt::Display for Provisioner {
|
|||
}
|
||||
|
||||
impl Provisioner {
|
||||
fn discover(&self) -> Vec<(String, Option<String>)> {
|
||||
pub fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
Provisioner::OpenPGPCard(o) => o.discover(),
|
||||
}
|
||||
}
|
||||
|
||||
fn provision(
|
||||
pub fn provision(
|
||||
&self,
|
||||
provisioner: config::Provisioner,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
Provisioner::OpenPGPCard(o) => o.provision(provisioner),
|
||||
Provisioner::OpenPGPCard(o) => {
|
||||
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
|
||||
type XPrv = ExtendedPrivateKey<Prv>;
|
||||
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
||||
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
|
||||
.chain_push(account_index);
|
||||
let mut client = keyforkd_client::Client::discover_socket()?;
|
||||
let xprv: XPrv = client.request_xprv(&path)?;
|
||||
o.provision(xprv, provisioner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provision_with_mnemonic(
|
||||
&self,
|
||||
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||
provisioner: config::Provisioner,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
Provisioner::OpenPGPCard(o) => {
|
||||
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
|
||||
type XPrv = ExtendedPrivateKey<Prv>;
|
||||
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
||||
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
|
||||
.chain_push(account_index);
|
||||
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
|
||||
o.provision(xprv, provisioner)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueEnum for Provisioner {
|
||||
fn value_variants<'a>() -> &'a [Self] {
|
||||
&[Self::OpenPGPCard(OpenPGPCard)]
|
||||
&[Self::OpenPGPCard(openpgp::OpenPGPCard)]
|
||||
}
|
||||
|
||||
fn to_possible_value(&self) -> Option<PossibleValue> {
|
||||
|
@ -45,39 +78,36 @@ impl ValueEnum for Provisioner {
|
|||
}
|
||||
}
|
||||
|
||||
trait ProvisionExec {
|
||||
/// Discover all known places the formatted key can be deployed to.
|
||||
fn discover(&self) -> Vec<(String, Option<String>)> {
|
||||
vec![]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("The given value could not be matched as a provisioner: {0} ({1})")]
|
||||
pub struct ProvisionerFromStrError(String, String);
|
||||
|
||||
impl std::str::FromStr for Provisioner {
|
||||
type Err = ProvisionerFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
<Provisioner as ValueEnum>::from_str(s, false)
|
||||
.map_err(|e| ProvisionerFromStrError(s.to_string(), e))
|
||||
}
|
||||
}
|
||||
|
||||
trait ProvisionExec {
|
||||
type PrivateKey: keyfork_derive_util::PrivateKey + Clone;
|
||||
|
||||
/// Discover all known places the formatted key can be deployed to.
|
||||
fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// Return the derivation path for deriving keys.
|
||||
fn derivation_prefix() -> keyfork_derive_util::DerivationPath;
|
||||
|
||||
/// Derive a key and deploy it to a target.
|
||||
fn provision(&self, p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OpenPGPCard;
|
||||
|
||||
impl ProvisionExec for OpenPGPCard {
|
||||
fn discover(&self) -> Vec<(String, Option<String>)> {
|
||||
/*
|
||||
vec![
|
||||
(
|
||||
"0006:26144195".to_string(),
|
||||
Some("Yubicats Heywood".to_string()),
|
||||
),
|
||||
(
|
||||
"0006:2614419y".to_string(),
|
||||
Some("Yubicats Heywood".to_string()),
|
||||
),
|
||||
]
|
||||
*/
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn provision(&self, _p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>> {
|
||||
todo!()
|
||||
}
|
||||
fn provision(
|
||||
&self,
|
||||
xprv: keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
|
||||
p: config::Provisioner,
|
||||
) -> Result<(), Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Clone, Debug)]
|
||||
|
@ -118,7 +148,6 @@ impl TryFrom<Provision> for config::Provisioner {
|
|||
|
||||
fn try_from(value: Provision) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
name: value.provisioner_name.to_string(),
|
||||
account: value.account_id.ok_or(MissingField("account_id"))?,
|
||||
identifier: value.identifier.ok_or(MissingField("identifier"))?,
|
||||
metadata: Default::default(),
|
||||
|
@ -130,7 +159,7 @@ impl Provision {
|
|||
pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match self.subcommand {
|
||||
Some(ProvisionSubcommands::Discover) => {
|
||||
let mut iter = self.provisioner_name.discover().into_iter().peekable();
|
||||
let mut iter = self.provisioner_name.discover()?.into_iter().peekable();
|
||||
while let Some((identifier, context)) = iter.next() {
|
||||
println!("Identifier: {identifier}");
|
||||
if let Some(context) = context {
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
use super::ProvisionExec;
|
||||
use crate::{config, openpgp_card::factory_reset_current_card};
|
||||
|
||||
use card_backend_pcsc::PcscBackend;
|
||||
use keyfork_derive_openpgp::{
|
||||
openpgp::{packet::UserID, types::KeyFlags},
|
||||
XPrv,
|
||||
};
|
||||
use keyfork_prompt::{
|
||||
default_handler, prompt_validated_passphrase,
|
||||
validators::{SecurePinValidator, Validator},
|
||||
};
|
||||
use openpgp_card_sequoia::{state::Open, Card};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OpenPGPCard;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Provisioner was unable to find a matching smartcard")]
|
||||
struct NoMatchingSmartcard;
|
||||
|
||||
impl ProvisionExec for OpenPGPCard {
|
||||
type PrivateKey = keyfork_derive_openpgp::XPrvKey;
|
||||
|
||||
fn discover(&self) -> Result<Vec<(String, Option<String>)>, Box<dyn std::error::Error>> {
|
||||
let mut idents = vec![];
|
||||
for backend in PcscBackend::cards(None)? {
|
||||
let backend = backend?;
|
||||
let mut card = Card::<Open>::new(backend)?;
|
||||
let mut transaction = card.transaction()?;
|
||||
let identifier = transaction.application_identifier()?.ident();
|
||||
let name = transaction.cardholder_name()?;
|
||||
let name = (!name.is_empty()).then_some(name);
|
||||
idents.push((identifier, name));
|
||||
}
|
||||
Ok(idents)
|
||||
}
|
||||
|
||||
fn derivation_prefix() -> keyfork_derive_util::DerivationPath {
|
||||
keyfork_derive_path_data::paths::OPENPGP.clone()
|
||||
}
|
||||
|
||||
fn provision(
|
||||
&self,
|
||||
xprv: XPrv,
|
||||
provisioner: config::Provisioner,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut pm = default_handler()?;
|
||||
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();
|
||||
|
||||
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,
|
||||
)?;
|
||||
|
||||
let mut has_provisioned = false;
|
||||
|
||||
for backend in PcscBackend::cards(None)? {
|
||||
let backend = backend?;
|
||||
|
||||
let subkeys = vec![
|
||||
KeyFlags::empty().set_certification(),
|
||||
KeyFlags::empty().set_signing(),
|
||||
KeyFlags::empty()
|
||||
.set_transport_encryption()
|
||||
.set_storage_encryption(),
|
||||
KeyFlags::empty().set_authentication(),
|
||||
];
|
||||
|
||||
// NOTE: This User ID doesn't have meaningful context on the card.
|
||||
// To give it a reasonable name, use `keyfork derive openpgp` or some other system that
|
||||
// generates the OpenPGP certificate.
|
||||
let userid = UserID::from("Keyfork-Provisioned Key");
|
||||
let cert = keyfork_derive_openpgp::derive(xprv.clone(), &subkeys, &userid)?;
|
||||
|
||||
let result = factory_reset_current_card(
|
||||
&mut |identifier| { identifier == provisioner.identifier },
|
||||
user_pin.trim(),
|
||||
admin_pin.trim(),
|
||||
&cert,
|
||||
&keyfork_derive_openpgp::openpgp::policy::StandardPolicy::new(),
|
||||
backend,
|
||||
)?;
|
||||
|
||||
has_provisioned = has_provisioned || result;
|
||||
}
|
||||
|
||||
if !has_provisioned {
|
||||
return Err(NoMatchingSmartcard)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
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 openpgp_card_sequoia::{state::Open, types::KeyType, Card};
|
||||
|
||||
use keyfork_derive_openpgp::{
|
||||
openpgp::{
|
||||
self,
|
||||
armor::{Kind, Writer},
|
||||
packet::{UserID, signature::SignatureBuilder},
|
||||
serialize::Marshal,
|
||||
types::{SignatureType, KeyFlags},
|
||||
packet::{signature::SignatureBuilder, UserID},
|
||||
policy::StandardPolicy,
|
||||
serialize::Marshal,
|
||||
types::{KeyFlags, SignatureType},
|
||||
Cert,
|
||||
},
|
||||
XPrv,
|
||||
|
@ -56,54 +56,6 @@ fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
|
|||
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(
|
||||
seen_cards: &mut HashSet<String>,
|
||||
user_pin: &str,
|
||||
admin_pin: &str,
|
||||
cert: &Cert,
|
||||
card_backend: PcscBackend,
|
||||
) -> 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");
|
||||
let mut card = Card::<Open>::new(card_backend)?;
|
||||
let mut transaction = card.transaction()?;
|
||||
let application_identifier = transaction.application_identifier()?.ident();
|
||||
if seen_cards.contains(&application_identifier) {
|
||||
// we were given the same card, error
|
||||
panic!("Previously used card {application_identifier} was reused");
|
||||
} else {
|
||||
seen_cards.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)?;
|
||||
transaction.change_user_pin("123456", user_pin)?;
|
||||
transaction.change_admin_pin("12345678", admin_pin)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Clone, Debug)]
|
||||
pub enum WizardSubcommands {
|
||||
GenerateShardSecret(GenerateShardSecret),
|
||||
|
@ -178,6 +130,8 @@ impl WizardSubcommands {
|
|||
|
||||
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)?
|
||||
|
@ -198,7 +152,7 @@ fn cross_sign_certs(certs: &mut [Cert]) -> Result<(), Box<dyn std::error::Error>
|
|||
let signature = sb.sign_userid_binding(
|
||||
&mut certify_key,
|
||||
signable_cert.primary_key().key(),
|
||||
&*userid,
|
||||
&userid,
|
||||
)?;
|
||||
let changed;
|
||||
(*signable_cert, changed) = signable_cert.clone().insert_packets2(signature)?;
|
||||
|
@ -266,10 +220,20 @@ impl GenerateShardSecret {
|
|||
&admin_pin_validator,
|
||||
)?;
|
||||
factory_reset_current_card(
|
||||
&mut seen_cards,
|
||||
&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,
|
||||
)?;
|
||||
}
|
||||
|
|
|
@ -2,20 +2,19 @@ use std::collections::HashMap;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Mnemonic {
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Provisioner {
|
||||
pub name: String,
|
||||
pub account: u32,
|
||||
pub identifier: String,
|
||||
pub metadata: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
pub mnemonic: Mnemonic,
|
||||
pub provisioner: Vec<Provisioner>,
|
||||
|
|
|
@ -10,6 +10,8 @@ use keyfork_bin::{Bin, ClosureBin};
|
|||
|
||||
mod cli;
|
||||
mod config;
|
||||
pub mod clap_ext;
|
||||
mod openpgp_card;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let bin = ClosureBin::new(|| {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
use card_backend_pcsc::PcscBackend;
|
||||
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
|
||||
use keyfork_derive_openpgp::openpgp::{Cert, policy::Policy};
|
||||
|
||||
/// Factory reset the current card so long as it does not match the last-used backend.
|
||||
///
|
||||
/// The return value of `false` means the filter was matched, whereas `true` means it was
|
||||
/// successfully provisioned.
|
||||
pub fn factory_reset_current_card(
|
||||
card_filter: &mut dyn FnMut(String) -> bool,
|
||||
user_pin: &str,
|
||||
admin_pin: &str,
|
||||
cert: &Cert,
|
||||
policy: &dyn Policy,
|
||||
card_backend: PcscBackend,
|
||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
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");
|
||||
let mut card = Card::<Open>::new(card_backend)?;
|
||||
let mut transaction = card.transaction()?;
|
||||
let application_identifier = transaction.application_identifier()?.ident();
|
||||
if !card_filter(application_identifier) {
|
||||
return Ok(false);
|
||||
}
|
||||
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)?;
|
||||
transaction.change_user_pin("123456", user_pin)?;
|
||||
transaction.change_admin_pin("12345678", admin_pin)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
Loading…
Reference in New Issue