superpower `keyfork mnemonic generate`

This commit is contained in:
Ryan Heywood 2025-01-27 11:59:44 -05:00
parent 8756c3d233
commit c232828290
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
9 changed files with 756 additions and 102 deletions

View File

@ -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,
})
}
}

View File

@ -1,6 +1,34 @@
use super::provision;
use super::Keyfork; use super::Keyfork;
use crate::{clap_ext::*, config};
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; 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)] #[derive(Clone, Debug, Default)]
pub enum SeedSize { pub enum SeedSize {
@ -96,21 +124,22 @@ impl std::str::FromStr for MnemonicSeedSource {
} }
impl 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 { let size = match size {
SeedSize::Bits128 => 128, SeedSize::Bits128 => 128,
SeedSize::Bits256 => 256, SeedSize::Bits256 => 256,
}; };
let seed = match self { let seed = match self {
MnemonicSeedSource::System => { MnemonicSeedSource::System => keyfork_entropy::generate_entropy_of_size(size / 8)?,
keyfork_entropy::generate_entropy_of_size(size / 8)?
}
MnemonicSeedSource::Playing => todo!(), MnemonicSeedSource::Playing => todo!(),
MnemonicSeedSource::Tarot => todo!(), MnemonicSeedSource::Tarot => todo!(),
MnemonicSeedSource::Dice => todo!(), MnemonicSeedSource::Dice => todo!(),
}; };
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?; 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 /// 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 /// 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. /// (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 { Generate {
/// The source from where a seed is created. /// The source from where a seed is created.
#[arg(long, value_enum, default_value_t = Default::default())] #[arg(long, value_enum, default_value_t = Default::default())]
@ -132,17 +165,423 @@ pub enum MnemonicSubcommands {
/// The size of the mnemonic, in bits. /// The size of the mnemonic, in bits.
#[arg(long, default_value_t = Default::default())] #[arg(long, default_value_t = Default::default())]
size: SeedSize, 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 { impl MnemonicSubcommands {
pub fn handle( pub fn handle(
&self, &self,
_m: &Mnemonic, _m: &Mnemonic,
_keyfork: &Keyfork, _keyfork: &Keyfork,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
match self { 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(())
}
} }
} }
} }

View File

@ -79,8 +79,7 @@ impl KeyforkCommands {
d.handle(keyfork)?; d.handle(keyfork)?;
} }
KeyforkCommands::Mnemonic(m) => { KeyforkCommands::Mnemonic(m) => {
let response = m.command.handle(m, keyfork)?; m.command.handle(m, keyfork)?;
println!("{response}");
} }
KeyforkCommands::Shard(s) => { KeyforkCommands::Shard(s) => {
s.command.handle(s, keyfork)?; s.command.handle(s, keyfork)?;

View File

@ -3,9 +3,15 @@ use crate::config;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_derive_util::{DerivationIndex, ExtendedPrivateKey};
mod openpgp;
type Identifier = (String, Option<String>);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Provisioner { pub enum Provisioner {
OpenPGPCard(OpenPGPCard), OpenPGPCard(openpgp::OpenPGPCard),
} }
impl std::fmt::Display for Provisioner { impl std::fmt::Display for Provisioner {
@ -17,25 +23,52 @@ impl std::fmt::Display for Provisioner {
} }
impl Provisioner { impl Provisioner {
fn discover(&self) -> Vec<(String, Option<String>)> { pub fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
match self { match self {
Provisioner::OpenPGPCard(o) => o.discover(), Provisioner::OpenPGPCard(o) => o.discover(),
} }
} }
fn provision( pub fn provision(
&self, &self,
provisioner: config::Provisioner, provisioner: config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
match self { 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 { impl ValueEnum for Provisioner {
fn value_variants<'a>() -> &'a [Self] { fn value_variants<'a>() -> &'a [Self] {
&[Self::OpenPGPCard(OpenPGPCard)] &[Self::OpenPGPCard(openpgp::OpenPGPCard)]
} }
fn to_possible_value(&self) -> Option<PossibleValue> { fn to_possible_value(&self) -> Option<PossibleValue> {
@ -45,39 +78,36 @@ impl ValueEnum for Provisioner {
} }
} }
trait ProvisionExec { #[derive(Debug, thiserror::Error)]
/// Discover all known places the formatted key can be deployed to. #[error("The given value could not be matched as a provisioner: {0} ({1})")]
fn discover(&self) -> Vec<(String, Option<String>)> { pub struct ProvisionerFromStrError(String, String);
vec![]
}
/// Derive a key and deploy it to a target. impl std::str::FromStr for Provisioner {
fn provision(&self, p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>>; 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))
}
} }
#[derive(Clone, Debug)] trait ProvisionExec {
pub struct OpenPGPCard; type PrivateKey: keyfork_derive_util::PrivateKey + Clone;
impl ProvisionExec for OpenPGPCard { /// Discover all known places the formatted key can be deployed to.
fn discover(&self) -> Vec<(String, Option<String>)> { fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
/* Ok(vec![])
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>> { /// Return the derivation path for deriving keys.
todo!() fn derivation_prefix() -> keyfork_derive_util::DerivationPath;
}
/// Derive a key and deploy it to a target.
fn provision(
&self,
xprv: keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
p: config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>>;
} }
#[derive(Subcommand, Clone, Debug)] #[derive(Subcommand, Clone, Debug)]
@ -118,7 +148,6 @@ impl TryFrom<Provision> for config::Provisioner {
fn try_from(value: Provision) -> Result<Self, Self::Error> { fn try_from(value: Provision) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
name: value.provisioner_name.to_string(),
account: value.account_id.ok_or(MissingField("account_id"))?, account: value.account_id.ok_or(MissingField("account_id"))?,
identifier: value.identifier.ok_or(MissingField("identifier"))?, identifier: value.identifier.ok_or(MissingField("identifier"))?,
metadata: Default::default(), metadata: Default::default(),
@ -130,7 +159,7 @@ impl Provision {
pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> { pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
match self.subcommand { match self.subcommand {
Some(ProvisionSubcommands::Discover) => { 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() { while let Some((identifier, context)) = iter.next() {
println!("Identifier: {identifier}"); println!("Identifier: {identifier}");
if let Some(context) = context { if let Some(context) = context {

View File

@ -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(())
}
}

View File

@ -1,18 +1,18 @@
use super::Keyfork; use super::Keyfork;
use crate::openpgp_card::factory_reset_current_card;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf}; use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
use card_backend_pcsc::PcscBackend; use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
use keyfork_derive_openpgp::{ use keyfork_derive_openpgp::{
openpgp::{ openpgp::{
self, self,
armor::{Kind, Writer}, armor::{Kind, Writer},
packet::{UserID, signature::SignatureBuilder}, packet::{signature::SignatureBuilder, UserID},
serialize::Marshal,
types::{SignatureType, KeyFlags},
policy::StandardPolicy, policy::StandardPolicy,
serialize::Marshal,
types::{KeyFlags, SignatureType},
Cert, Cert,
}, },
XPrv, XPrv,
@ -56,54 +56,6 @@ fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
Ok(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)] #[derive(Subcommand, Clone, Debug)]
pub enum WizardSubcommands { pub enum WizardSubcommands {
GenerateShardSecret(GenerateShardSecret), GenerateShardSecret(GenerateShardSecret),
@ -178,6 +130,8 @@ impl WizardSubcommands {
fn cross_sign_certs(certs: &mut [Cert]) -> Result<(), Box<dyn std::error::Error>> { fn cross_sign_certs(certs: &mut [Cert]) -> Result<(), Box<dyn std::error::Error>> {
let policy = StandardPolicy::new(); let policy = StandardPolicy::new();
#[allow(clippy::unnecessary_to_owned)]
for signing_cert in certs.to_vec() { for signing_cert in certs.to_vec() {
let mut certify_key = signing_cert let mut certify_key = signing_cert
.with_policy(&policy, None)? .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( let signature = sb.sign_userid_binding(
&mut certify_key, &mut certify_key,
signable_cert.primary_key().key(), signable_cert.primary_key().key(),
&*userid, &userid,
)?; )?;
let changed; let changed;
(*signable_cert, changed) = signable_cert.clone().insert_packets2(signature)?; (*signable_cert, changed) = signable_cert.clone().insert_packets2(signature)?;
@ -266,10 +220,20 @@ impl GenerateShardSecret {
&admin_pin_validator, &admin_pin_validator,
)?; )?;
factory_reset_current_card( 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(), user_pin.trim(),
admin_pin.trim(), admin_pin.trim(),
&cert, &cert,
&openpgp::policy::NullPolicy::new(),
card_backend, card_backend,
)?; )?;
} }

View File

@ -2,20 +2,19 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone)]
pub struct Mnemonic { pub struct Mnemonic {
pub hash: String, pub hash: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone)]
pub struct Provisioner { pub struct Provisioner {
pub name: String,
pub account: u32, pub account: u32,
pub identifier: String, pub identifier: String,
pub metadata: Option<HashMap<String, String>>, pub metadata: Option<HashMap<String, String>>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone)]
pub struct Config { pub struct Config {
pub mnemonic: Mnemonic, pub mnemonic: Mnemonic,
pub provisioner: Vec<Provisioner>, pub provisioner: Vec<Provisioner>,

View File

@ -10,6 +10,8 @@ use keyfork_bin::{Bin, ClosureBin};
mod cli; mod cli;
mod config; mod config;
pub mod clap_ext;
mod openpgp_card;
fn main() -> ExitCode { fn main() -> ExitCode {
let bin = ClosureBin::new(|| { let bin = ClosureBin::new(|| {

View File

@ -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)
}