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 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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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![]
|
|
||||||
|
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.
|
/// Derive a key and deploy it to a target.
|
||||||
fn provision(&self, p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>>;
|
fn provision(
|
||||||
}
|
&self,
|
||||||
|
xprv: keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
|
||||||
#[derive(Clone, Debug)]
|
p: config::Provisioner,
|
||||||
pub struct OpenPGPCard;
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
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!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 {
|
||||||
|
|
|
@ -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 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,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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(|| {
|
||||||
|
|
|
@ -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