keyfork: restructure wizard shard key generation

also: `keyfork provision shard`
This commit is contained in:
Ryan Heywood 2025-02-25 17:02:35 -05:00
parent 674e2e93c5
commit a1c3d52c14
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
3 changed files with 156 additions and 91 deletions

View File

@ -12,20 +12,27 @@ type Identifier = (String, Option<String>);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Provisioner { pub enum Provisioner {
OpenPGPCard(openpgp::OpenPGPCard), OpenPGPCard(openpgp::OpenPGPCard),
Shard(openpgp::Shard),
} }
impl std::fmt::Display for Provisioner { impl std::fmt::Display for Provisioner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { f.write_str(self.identifier())
Provisioner::OpenPGPCard(_) => f.write_str("openpgp-card"),
}
} }
} }
impl Provisioner { impl Provisioner {
pub fn identifier(&self) -> &'static str {
match self {
Provisioner::OpenPGPCard(_) => "openpgp-card",
Provisioner::Shard(_) => "shard",
}
}
pub fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> { 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(),
Provisioner::Shard(s) => s.discover(),
} }
} }
@ -44,6 +51,17 @@ impl Provisioner {
let xprv: XPrv = client.request_xprv(&path)?; let xprv: XPrv = client.request_xprv(&path)?;
o.provision(xprv, provisioner) o.provision(xprv, provisioner)
} }
Provisioner::Shard(s) => {
type Prv = <openpgp::Shard as ProvisionExec>::PrivateKey;
type XPrv = ExtendedPrivateKey<Prv>;
let account_index = DerivationIndex::new(provisioner.account, true)?;
let path = <openpgp::Shard as ProvisionExec>::derivation_prefix()
.chain_push(account_index);
let mut client = keyforkd_client::Client::discover_socket()?;
let xprv: XPrv = client.request_xprv(&path)?;
panic!();
s.provision(xprv, provisioner)
}
} }
} }
@ -62,19 +80,26 @@ impl Provisioner {
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?; let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
o.provision(xprv, provisioner) o.provision(xprv, provisioner)
} }
Provisioner::Shard(s) => {
type Prv = <openpgp::Shard as ProvisionExec>::PrivateKey;
type XPrv = ExtendedPrivateKey<Prv>;
let account_index = DerivationIndex::new(provisioner.account, true)?;
let path = <openpgp::Shard as ProvisionExec>::derivation_prefix()
.chain_push(account_index);
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
s.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(openpgp::OpenPGPCard)] &[Self::OpenPGPCard(openpgp::OpenPGPCard), Self::Shard(openpgp::Shard)]
} }
fn to_possible_value(&self) -> Option<PossibleValue> { fn to_possible_value(&self) -> Option<PossibleValue> {
Some(PossibleValue::new(match self { Some(PossibleValue::new(self.identifier()))
Self::OpenPGPCard(_) => "openpgp-card",
}))
} }
} }

View File

@ -18,28 +18,102 @@ use keyfork_prompt::default_handler;
use openpgp_card_sequoia::{state::Open, Card}; use openpgp_card_sequoia::{state::Open, Card};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct OpenPGPCard;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
#[error("Provisioner was unable to find a matching smartcard")] #[error("Provisioner was unable to find a matching smartcard")]
struct NoMatchingSmartcard; struct NoMatchingSmartcard;
fn discover_cards() -> 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 provision_card(
provisioner: config::Provisioner,
xprv: XPrv,
) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = default_handler()?;
let (user_pin, admin_pin) = get_new_pins(&mut *pm)?;
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let userid = match provisioner.metadata.as_ref().and_then(|m| m.get("userid")) {
Some(userid) => UserID::from(userid.as_str()),
None => UserID::from("Keyfork-Provisioned Key"),
};
let cert = keyfork_derive_openpgp::derive(xprv.clone(), &subkeys, &userid)?;
if !provisioner
.metadata
.as_ref()
.is_some_and(|m| m.contains_key("_skip_cert_output"))
{
let cert_output = match provisioner.metadata.as_ref().and_then(|m| m.get("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(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,
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(())
}
#[derive(Clone, Debug)]
pub struct OpenPGPCard;
impl ProvisionExec for OpenPGPCard { impl ProvisionExec for OpenPGPCard {
type PrivateKey = keyfork_derive_openpgp::XPrvKey; type PrivateKey = keyfork_derive_openpgp::XPrvKey;
fn discover(&self) -> Result<Vec<(String, Option<String>)>, Box<dyn std::error::Error>> { fn discover(&self) -> Result<Vec<(String, Option<String>)>, Box<dyn std::error::Error>> {
let mut idents = vec![]; discover_cards()
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 { fn derivation_prefix() -> keyfork_derive_util::DerivationPath {
@ -51,69 +125,29 @@ impl ProvisionExec for OpenPGPCard {
xprv: XPrv, xprv: XPrv,
provisioner: config::Provisioner, provisioner: config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = default_handler()?; provision_card(provisioner, xprv)
}
let (user_pin, admin_pin) = get_new_pins(&mut *pm)?; }
let subkeys = vec![ #[derive(Clone, Debug)]
KeyFlags::empty().set_certification(), pub struct Shard;
KeyFlags::empty().set_signing(),
KeyFlags::empty() impl ProvisionExec for Shard {
.set_transport_encryption() type PrivateKey = keyfork_derive_openpgp::XPrvKey;
.set_storage_encryption(),
KeyFlags::empty().set_authentication(), fn discover(&self) -> Result<Vec<(String, Option<String>)>, Box<dyn std::error::Error>> {
]; discover_cards()
}
let userid = match provisioner.metadata.as_ref().and_then(|m| m.get("userid")) {
Some(userid) => UserID::from(userid.as_str()), fn derivation_prefix() -> keyfork_derive_util::DerivationPath {
None => UserID::from("Keyfork-Provisioned Key"), keyfork_derive_path_data::paths::OPENPGP_SHARD.clone()
}; }
let cert = keyfork_derive_openpgp::derive(xprv.clone(), &subkeys, &userid)?;
fn provision(
if !provisioner &self,
.metadata xprv: XPrv,
.as_ref() provisioner: config::Provisioner,
.is_some_and(|m| m.contains_key("_skip_cert_output")) ) -> Result<(), Box<dyn std::error::Error>> {
{ provision_card(provisioner, xprv)
let cert_output = match provisioner.metadata.as_ref().and_then(|m| m.get("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(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,
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

@ -34,9 +34,7 @@ pub struct PinLength(usize);
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
// TODO: refactor to use mnemonic derived seed instead of 256 bit entropy to allow for possible fn derive_key(seed: [u8; 64], index: u8) -> Result<Cert> {
// recovery in the future.
fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
let subkeys = vec![ let subkeys = vec![
KeyFlags::empty().set_certification(), KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(), KeyFlags::empty().set_signing(),
@ -167,7 +165,9 @@ fn cross_sign_certs(certs: &mut [Cert]) -> Result<(), Box<dyn std::error::Error>
impl GenerateShardSecret { impl GenerateShardSecret {
fn handle(&self) -> Result<()> { fn handle(&self) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?; let root_entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mnemonic = Mnemonic::from_array(root_entropy);
let seed = mnemonic.generate_seed(None);
let mut pm = default_handler()?; let mut pm = default_handler()?;
let mut certs = vec![]; let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new(); let mut seen_cards: HashSet<String> = HashSet::new();
@ -246,12 +246,18 @@ impl GenerateShardSecret {
if let Some(output_file) = self.output.as_ref() { if let Some(output_file) = self.output.as_ref() {
let output = File::create(output_file)?; let output = File::create(output_file)?;
opgp.shard_and_encrypt(self.threshold, certs.len() as u8, &seed, &certs[..], output)?; opgp.shard_and_encrypt(
self.threshold,
certs.len() as u8,
mnemonic.as_bytes(),
&certs[..],
output,
)?;
} else { } else {
opgp.shard_and_encrypt( opgp.shard_and_encrypt(
self.threshold, self.threshold,
certs.len() as u8, certs.len() as u8,
&seed, mnemonic.as_bytes(),
&certs[..], &certs[..],
std::io::stdout(), std::io::stdout(),
)?; )?;