keyfork mnemonic generate: feedback improvements

* Touch policy is now set to `on` by default (not fixed, as that's
  irreversible).
* The value passed to `--encrypt-to-self` is the actual encrypted
  output.
* The `cert_output` passed to `--encrypt-to-self` by default is the
  fingerprint of the certificate.
* The OpenPGP provisioner can now be used without identifiers, if the
  correct amount of smartcards are actively plugged into the current
  system.
* The OpenPGP provisioner, when run without `--encrypt-to-self`, will
  output the OpenPGP certificate for the smartcard.
This commit is contained in:
Ryan Heywood 2025-02-19 20:12:27 -05:00
parent 0243212c80
commit db19b30bfe
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
3 changed files with 124 additions and 53 deletions

View File

@ -87,6 +87,7 @@ impl From<&SeedSize> for usize {
}
}
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum MnemonicSeedSourceParseError {
#[error("Expected one of system, playing, tarot, dice")]
@ -143,6 +144,18 @@ impl MnemonicSeedSource {
}
}
/// An error occurred while performing an operation.
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// An error occurred when interacting iwth a file.
#[error("Error while performing IO operation on: {1}")]
IOContext(#[source] std::io::Error, PathBuf),
}
fn context_stub<'a>(path: &'a Path) -> impl Fn(std::io::Error) -> Error + 'a {
|e| Error::IOContext(e, path.to_path_buf())
}
#[derive(Subcommand, Clone, Debug)]
pub enum MnemonicSubcommands {
/// Generate a mnemonic using a given entropy source.
@ -206,12 +219,12 @@ pub enum MnemonicSubcommands {
/// `--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
/// When given arguments in the format `--encrypt-to-self encrypted.asc,output=cert.asc`,
/// the output of the OpenPGP certificate will be written to `cert.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.
/// default behavior is to write the certificate to a file named after the certificate's
/// fingerprint. 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
@ -296,7 +309,7 @@ fn do_encrypt_to(
literal_message.write_all(b"\n")?;
literal_message.finalize()?;
let mut file = File::create_new(&output_file)?;
let mut file = File::create_new(&output_file).map_err(context_stub(&output_file))?;
if is_armored {
let mut writer = Writer::new(file, Kind::Message)?;
writer.write_all(&output)?;
@ -313,11 +326,6 @@ fn do_encrypt_to_self(
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))
@ -350,23 +358,28 @@ fn do_encrypt_to_self(
&userid.unwrap_or(UserID::from("Keyfork-Generated Key")),
)?;
let mut file = File::create_new(path)?;
if is_armored {
let cert_path = match options.get("output") {
Some(path) => PathBuf::from(path),
None => {
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
eprintln!(
"Writing OpenPGP certificate to default path: {path}",
path = path.display()
);
path
}
};
let file = File::create_new(&cert_path).map_err(context_stub(&cert_path))?;
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(),
)]),
&cert_path,
&StringMap::from([(String::from("output"), path.to_string_lossy().to_string())]),
)?;
Ok(())
@ -421,7 +434,7 @@ fn do_shard(
let mut output = vec![];
openpgp.shard_and_encrypt(threshold, max, mnemonic.as_bytes(), &certs[..], &mut output)?;
let mut file = File::create_new(&output_file)?;
let mut file = File::create_new(&output_file).map_err(context_stub(&output_file))?;
if is_armored {
file.write_all(&output)?;
} else {
@ -466,7 +479,7 @@ fn do_shard_to(
&mut output,
)?;
let mut file = File::create_new(&output_file)?;
let mut file = File::create_new(&output_file).map_err(context_stub(&output_file))?;
if is_armored {
file.write_all(&output)?;
} else {
@ -482,10 +495,6 @@ fn do_shard_to(
Ok(())
}
#[derive(thiserror::Error, Debug)]
#[error("missing key: {0}")]
struct MissingKey(&'static str);
fn do_provision(
mnemonic: &keyfork_mnemonic::Mnemonic,
provisioner: &provision::Provisioner,
@ -499,16 +508,27 @@ fn do_provision(
.unwrap_or(0);
let identifier = options
.remove("identifier")
.ok_or(MissingKey("identifier"))?
.split(',')
.map(String::from)
.collect::<Vec<_>>();
.map(|s| s.split('.').map(String::from).collect::<Vec<_>>())
.map(Result::<_, Box<dyn std::error::Error>>::Ok)
.unwrap_or_else(|| {
Ok(provisioner
.discover()?
.into_iter()
.map(|(identifier, _)| identifier)
.collect())
})?;
let count = options
.remove("count")
.map(|count| usize::from_str(&count))
.transpose()?
.unwrap_or(identifier.len());
assert_eq!(
count,
identifier.len(),
"amount of identifiers discovered or provided did not match provisioner count"
);
for (_, identifier) in (0..count).zip(identifier.into_iter()) {
let provisioner_config = config::Provisioner {
account,
@ -562,7 +582,20 @@ impl MnemonicSubcommands {
}
if let Some(provisioner) = provision {
do_provision(&mnemonic, &provisioner.inner, &provisioner.values)?;
// NOTE: If we have encrypt_to_self, we likely also have the certificate
// already generated. Therefore, we can skip generating it in the provisioner.
// However, if we don't have encrypt_to_self, we might not have the
// certificate, therefore the provisioner - by default - generates the public
// key output.
//
// We use the atypical `_skip_cert_output` field here to denote an automatic
// marking to skip the cert output. However, the `cert_output` field will take
// priority, since it can only be manually set by the user.
let mut values = provisioner.values.clone();
if encrypt_to_self.is_some() {
values.insert(String::from("_skip_cert_output"), String::from("1"));
}
do_provision(&mnemonic, &provisioner.inner, &values)?;
}
if let Some(shard_to) = shard_to {

View File

@ -3,7 +3,12 @@ use crate::{config, openpgp_card::factory_reset_current_card};
use card_backend_pcsc::PcscBackend;
use keyfork_derive_openpgp::{
openpgp::{packet::UserID, types::KeyFlags},
openpgp::{
armor::{Kind, Writer},
packet::UserID,
serialize::Serialize,
types::KeyFlags,
},
XPrv,
};
use keyfork_prompt::{
@ -11,6 +16,7 @@ use keyfork_prompt::{
validators::{SecurePinValidator, Validator},
};
use openpgp_card_sequoia::{state::Open, Card};
use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct OpenPGPCard;
@ -70,11 +76,6 @@ impl ProvisionExec for OpenPGPCard {
&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(),
@ -90,8 +91,42 @@ impl ProvisionExec for OpenPGPCard {
let userid = UserID::from("Keyfork-Provisioned Key");
let cert = keyfork_derive_openpgp::derive(xprv.clone(), &subkeys, &userid)?;
// cert_output is never automatically set, but _skip_cert_output is, so we bypass the
// automatically generated _skip_cert_output
if !provisioner
.metadata
.as_ref()
.is_some_and(|m| m.contains_key("_skip_cert_output") && !m.contains_key("cert_output"))
{
let cert_output = match provisioner
.metadata
.as_ref()
.and_then(|m| m.get("cert_output"))
{
Some(cert_output) => PathBuf::from(cert_output),
None => {
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
eprintln!(
"Writing OpenPGP certificate to: {path}",
path = path.display()
);
path
}
};
let cert_output_file = std::fs::File::create_new(cert_output)?;
let mut writer = Writer::new(cert_output_file, Kind::PublicKey)?;
cert.serialize(&mut writer)?;
writer.finalize()?;
}
let mut has_provisioned = false;
for backend in PcscBackend::cards(None)? {
let backend = backend?;
let result = factory_reset_current_card(
&mut |identifier| { identifier == provisioner.identifier },
&mut |identifier| identifier == provisioner.identifier,
user_pin.trim(),
admin_pin.trim(),
&cert,

View File

@ -1,5 +1,5 @@
use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
use openpgp_card_sequoia::{state::Open, types::KeyType, Card, types::TouchPolicy};
use keyfork_derive_openpgp::openpgp::{Cert, policy::Policy};
/// Factory reset the current card so long as it does not match the last-used backend.
@ -42,8 +42,11 @@ pub fn factory_reset_current_card(
transaction.factory_reset()?;
let mut admin = transaction.to_admin_card("12345678")?;
admin.upload_key(signing_key, KeyType::Signing, None)?;
admin.set_touch_policy(KeyType::Signing, TouchPolicy::On)?;
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
admin.set_touch_policy(KeyType::Decryption, TouchPolicy::On)?;
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
admin.set_touch_policy(KeyType::Authentication, TouchPolicy::On)?;
transaction.change_user_pin("123456", user_pin)?;
transaction.change_admin_pin("12345678", admin_pin)?;
Ok(true)