diff --git a/Cargo.lock b/Cargo.lock index 1b4c380..87e2629 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,7 @@ dependencies = [ "rand 0.7.3", "secrecy", "sha2", + "which", "x509", "x509-parser", "yubikey-piv", @@ -353,6 +354,12 @@ dependencies = [ "signature", ] +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "elliptic-curve" version = "0.8.5" @@ -1212,6 +1219,16 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "which" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe" +dependencies = [ + "either", + "libc", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 869a7e0..69c2d7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ pcsc = "2.4" rand = "0.7" secrecy = "0.7" sha2 = "0.9" +which = "4.1" x509 = "0.2" x509-parser = "0.9" yubikey-piv = { version = "0.3", features = ["untested"] } diff --git a/examples/generate-docs.rs b/examples/generate-docs.rs index 4e5b609..99c42d2 100644 --- a/examples/generate-docs.rs +++ b/examples/generate-docs.rs @@ -49,18 +49,18 @@ fn main() { Flag::new() .short("-i") .long("--identity") - .help("Print the identity stored in a YubiKey slot."), + .help("Print identities stored in connected YubiKeys."), ) .flag( Flag::new() .short("-l") .long("--list") - .help("List all age identities in connected YubiKeys."), + .help("List recipients for age identities in connected YubiKeys."), ) .flag( Flag::new() .long("--list-all") - .help("List all YubiKey keys that are compatible with age."), + .help("List recipients for all YubiKey keys that are compatible with age."), ) .flag( Flag::new() diff --git a/src/builder.rs b/src/builder.rs index eab165b..aaf1538 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -10,13 +10,13 @@ use yubikey_piv::{ use crate::{ error::Error, p256::Recipient, - util::POLICY_EXTENSION_OID, + util::{Metadata, POLICY_EXTENSION_OID}, yubikey::{self, Stub}, BINARY_NAME, USABLE_SLOTS, }; -const DEFAULT_PIN_POLICY: PinPolicy = PinPolicy::Once; -const DEFAULT_TOUCH_POLICY: TouchPolicy = TouchPolicy::Always; +pub(crate) const DEFAULT_PIN_POLICY: PinPolicy = PinPolicy::Once; +pub(crate) const DEFAULT_TOUCH_POLICY: TouchPolicy = TouchPolicy::Always; pub(crate) struct IdentityBuilder { slot: Option, @@ -57,7 +57,7 @@ impl IdentityBuilder { self } - pub(crate) fn build(self, yubikey: &mut YubiKey) -> Result<(Stub, Recipient, String), Error> { + pub(crate) fn build(self, yubikey: &mut YubiKey) -> Result<(Stub, Recipient, Metadata), Error> { let slot = match self.slot { Some(slot) => { if !self.force { @@ -141,12 +141,12 @@ impl IdentityBuilder { )?; let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref()).unwrap(); - let created = cert.validity().not_before.to_rfc2822(); + let metadata = Metadata::extract(yubikey, slot, &cert, false).unwrap(); Ok(( Stub::new(yubikey.serial(), slot, &recipient), recipient, - created, + metadata, )) } } diff --git a/src/error.rs b/src/error.rs index 2f5ed81..05d0ffb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,20 +6,21 @@ use crate::util::slot_to_ui; pub enum Error { CustomManagementKey, + InvalidFlagCommand(String, String), + InvalidFlagTui(String), InvalidPinLength, InvalidPinPolicy(String), InvalidSlot(u8), InvalidTouchPolicy(String), Io(io::Error), MultipleCommands, - MultipleIdentities, MultipleYubiKeys, NoEmptySlots(Serial), - NoIdentities, NoMatchingSerial(Serial), SlotHasNoIdentity(RetiredSlotId), SlotIsNotEmpty(RetiredSlotId), TimedOut, + UseListForSingleSlot, YubiKey(yubikey_piv::Error), } @@ -43,6 +44,14 @@ impl fmt::Debug for Error { Error::CustomManagementKey => { writeln!(f, "Custom unprotected management keys are not supported.")? } + Error::InvalidFlagCommand(flag, command) => { + writeln!(f, "Flag '{}' cannot be used with '{}'.", flag, command)? + } + Error::InvalidFlagTui(flag) => writeln!( + f, + "Flag '{}' cannot be used with the interactive interface.", + flag + )?, Error::InvalidPinLength => writeln!(f, "The PIN needs to be 1-8 characters.")?, Error::InvalidPinPolicy(s) => writeln!( f, @@ -64,10 +73,6 @@ impl fmt::Debug for Error { f, "Only one of --generate, --identity, --list, --list-all can be specified." )?, - Error::MultipleIdentities => writeln!( - f, - "This YubiKey has multiple age identities. Use --slot to select a single identity." - )?, Error::MultipleYubiKeys => writeln!( f, "Multiple YubiKeys are plugged in. Use --serial to select a single YubiKey." @@ -75,9 +80,6 @@ impl fmt::Debug for Error { Error::NoEmptySlots(serial) => { writeln!(f, "YubiKey with serial {} has no empty slots.", serial)? } - Error::NoIdentities => { - writeln!(f, "This YubiKey does not contain any age identities.")? - } Error::NoMatchingSerial(serial) => { writeln!(f, "Could not find YubiKey with serial {}.", serial)? } @@ -94,6 +96,9 @@ impl fmt::Debug for Error { Error::TimedOut => { writeln!(f, "Timed out while waiting for a YubiKey to be inserted.")? } + Error::UseListForSingleSlot => { + writeln!(f, "Use --list to print the recipient for a single slot.")? + } Error::YubiKey(e) => match e { yubikey_piv::error::Error::NotFound => { writeln!(f, "Please insert the YubiKey you want to set up")? diff --git a/src/format.rs b/src/format.rs index 723abcd..491f81d 100644 --- a/src/format.rs +++ b/src/format.rs @@ -9,7 +9,7 @@ use std::convert::TryInto; use crate::{p256::Recipient, STANZA_TAG}; -pub(crate) const STANZA_KEY_LABEL: &[u8] = b"age-encryption.org/v1/piv-p256"; +pub(crate) const STANZA_KEY_LABEL: &[u8] = b"piv-p256"; const TAG_BYTES: usize = 4; const EPK_BYTES: usize = 33; diff --git a/src/main.rs b/src/main.rs index 697387e..d2ed3be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,15 @@ +use std::convert::{TryFrom, TryInto}; +use std::fs::{File, OpenOptions}; +use std::io::{self, Write}; + use age_plugin::run_state_machine; -use dialoguer::{Confirm, Select}; +use dialoguer::{Confirm, Input, Select}; use gumdrop::Options; use yubikey_piv::{ certificate::PublicKeyInfo, key::{RetiredSlotId, SlotId}, policy::{PinPolicy, TouchPolicy}, - Key, Readers, + Key, Readers, Serial, }; mod builder; @@ -68,13 +72,16 @@ struct PluginOptions { #[options(help = "Generate a new YubiKey identity.")] generate: bool, - #[options(help = "Print the identity stored in a YubiKey slot.")] + #[options(help = "Print identities stored in connected YubiKeys.")] identity: bool, - #[options(help = "List all age identities in connected YubiKeys.")] + #[options(help = "List recipients for age identities in connected YubiKeys.")] list: bool, - #[options(help = "List all YubiKey keys that are compatible with age.", no_short)] + #[options( + help = "List recipients for all YubiKey keys that are compatible with age.", + no_short + )] list_all: bool, #[options( @@ -105,36 +112,61 @@ struct PluginOptions { touch_policy: Option, } -fn generate(opts: PluginOptions) -> Result<(), Error> { - let serial = opts.serial.map(|s| s.into()); - let slot = opts.slot.map(util::ui_to_slot).transpose()?; - let pin_policy = opts - .pin_policy - .map(util::pin_policy_from_string) - .transpose()?; - let touch_policy = opts - .touch_policy - .map(util::touch_policy_from_string) - .transpose()?; +struct PluginFlags { + serial: Option, + slot: Option, + name: Option, + pin_policy: Option, + touch_policy: Option, + force: bool, +} - let mut yubikey = yubikey::open(serial)?; +impl TryFrom for PluginFlags { + type Error = Error; - let (stub, recipient, created) = builder::IdentityBuilder::new(slot) - .with_name(opts.name) - .with_pin_policy(pin_policy) - .with_touch_policy(touch_policy) - .force(opts.force) + fn try_from(opts: PluginOptions) -> Result { + let serial = opts.serial.map(|s| s.into()); + let slot = opts.slot.map(util::ui_to_slot).transpose()?; + let pin_policy = opts + .pin_policy + .map(util::pin_policy_from_string) + .transpose()?; + let touch_policy = opts + .touch_policy + .map(util::touch_policy_from_string) + .transpose()?; + + Ok(PluginFlags { + serial, + slot, + name: opts.name, + pin_policy, + touch_policy, + force: opts.force, + }) + } +} + +fn generate(flags: PluginFlags) -> Result<(), Error> { + let mut yubikey = yubikey::open(flags.serial)?; + + let (stub, recipient, metadata) = builder::IdentityBuilder::new(flags.slot) + .with_name(flags.name) + .with_pin_policy(flags.pin_policy) + .with_touch_policy(flags.touch_policy) + .force(flags.force) .build(&mut yubikey)?; - util::print_identity(stub, recipient, &created); + util::print_identity(stub, recipient, metadata); Ok(()) } -fn identity(opts: PluginOptions) -> Result<(), Error> { - let serial = opts.serial.map(|s| s.into()); - let slot = opts.slot.map(util::ui_to_slot).transpose()?; - +fn print_single( + serial: Option, + slot: RetiredSlotId, + printer: impl Fn(yubikey::Stub, p256::Recipient, util::Metadata), +) -> Result<(), Error> { let mut yubikey = yubikey::open(serial)?; let mut keys = Key::list(&mut yubikey)?.into_iter().filter_map(|key| { @@ -148,46 +180,37 @@ fn identity(opts: PluginOptions) -> Result<(), Error> { } }); - let (key, slot, recipient) = if let Some(slot) = slot { - keys.find(|(_, s, _)| s == &slot) - .ok_or(Error::SlotHasNoIdentity(slot)) - } else { - let mut keys = keys.filter(|(key, _, _)| { - let cert = x509_parser::parse_x509_certificate(key.certificate().as_ref()) - .map(|(_, cert)| cert) - .ok(); - match cert - .as_ref() - .and_then(|cert| cert.subject().iter_organization().next()) - { - Some(org) => org.as_str() == Ok(BINARY_NAME), - _ => false, - } - }); - match (keys.next(), keys.next()) { - (None, None) => Err(Error::NoIdentities), - (Some(key), None) => Ok(key), - (Some(_), Some(_)) => Err(Error::MultipleIdentities), - (None, Some(_)) => unreachable!(), - } - }?; + let (key, slot, recipient) = keys + .find(|(_, s, _)| s == &slot) + .ok_or(Error::SlotHasNoIdentity(slot))?; let stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient); - let created = x509_parser::parse_x509_certificate(key.certificate().as_ref()) + let metadata = x509_parser::parse_x509_certificate(key.certificate().as_ref()) .ok() - .map(|(_, cert)| cert.validity().not_before.to_rfc2822()) - .unwrap_or_else(|| "Unknown".to_owned()); + .and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, true)) + .unwrap(); - util::print_identity(stub, recipient, &created); + printer(stub, recipient, metadata); Ok(()) } -fn list(all: bool) -> Result<(), Error> { +fn print_multiple( + kind: &str, + serial: Option, + all: bool, + printer: impl Fn(yubikey::Stub, p256::Recipient, util::Metadata), +) -> Result<(), Error> { let mut readers = Readers::open()?; + let mut printed = 0; for reader in readers.iter()?.filter(yubikey::filter_connected) { let mut yubikey = reader.open()?; + if let Some(serial) = serial { + if yubikey.serial() != serial { + continue; + } + } for key in Key::list(&mut yubikey)? { // We only use the retired slots. @@ -205,38 +228,71 @@ fn list(all: bool) -> Result<(), Error> { _ => continue, }; - let ((name, pin_policy, touch_policy), created) = - match x509_parser::parse_x509_certificate(key.certificate().as_ref()) - .ok() - .and_then(|(_, cert)| { - util::extract_name_and_policies(&mut yubikey, &key, &cert, all) - .map(|res| (res, cert.validity().not_before.to_rfc2822())) - }) { - Some(res) => res, - None => continue, - }; + let stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient); + let metadata = match x509_parser::parse_x509_certificate(key.certificate().as_ref()) + .ok() + .and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, all)) + { + Some(res) => res, + None => continue, + }; - println!( - "# Serial: {}, Slot: {}", - yubikey.serial(), - util::slot_to_ui(&slot), - ); - println!("# Name: {}", name); - println!("# Created: {}", created); - println!("# PIN policy: {}", util::pin_policy_to_str(pin_policy)); - println!( - "# Touch policy: {}", - util::touch_policy_to_str(touch_policy) - ); - println!("{}", recipient.to_string()); + printer(stub, recipient, metadata); + printed += 1; println!(); } println!(); } + if printed > 1 { + eprintln!( + "Generated {} for {} slots. If you intended to select a slot, use --slot.", + kind, printed, + ); + } Ok(()) } +fn print_details( + kind: &str, + flags: PluginFlags, + all: bool, + printer: impl Fn(yubikey::Stub, p256::Recipient, util::Metadata), +) -> Result<(), Error> { + if let Some(slot) = flags.slot { + print_single(flags.serial, slot, printer) + } else { + print_multiple(kind, flags.serial, all, printer) + } +} + +fn identity(flags: PluginFlags) -> Result<(), Error> { + if flags.force { + return Err(Error::InvalidFlagCommand( + "--force".into(), + "--identity".into(), + )); + } + print_details("identities", flags, false, util::print_identity) +} + +fn list(flags: PluginFlags, all: bool) -> Result<(), Error> { + if all && flags.slot.is_some() { + return Err(Error::UseListForSingleSlot); + } + if flags.force { + return Err(Error::InvalidFlagCommand( + "--force".into(), + format!("--list{}", if all { "-all" } else { "" }), + )); + } + + print_details("recipients", flags, all, |_, recipient, metadata| { + println!("{}", metadata); + println!("{}", recipient.to_string()); + }) +} + fn main() -> Result<(), Error> { env_logger::builder() .format_timestamp(None) @@ -266,14 +322,19 @@ fn main() -> Result<(), Error> { println!("age-plugin-yubikey {}", env!("CARGO_PKG_VERSION")); Ok(()) } else if opts.generate { - generate(opts) + generate(opts.try_into()?) } else if opts.identity { - identity(opts) + identity(opts.try_into()?) } else if opts.list { - list(false) + list(opts.try_into()?, false) } else if opts.list_all { - list(true) + list(opts.try_into()?, true) } else { + if opts.force { + return Err(Error::InvalidFlagTui("--force".into())); + } + let flags: PluginFlags = opts.try_into()?; + eprintln!("✨ Let's get your YubiKey set up for age! ✨"); eprintln!(); eprintln!("This tool can create a new age identity in a free slot of your YubiKey."); @@ -283,9 +344,7 @@ fn main() -> Result<(), Error> { eprintln!(" age-plugin-yubikey --generate"); eprintln!(); eprintln!("If you are already using a YubiKey with age, you can select an existing"); - eprintln!("slot to recreate its corresponding identity file and recipient. You can"); - eprintln!("also obtain this directly with:"); - eprintln!(" age-plugin-yubikey --identity"); + eprintln!("slot to recreate its corresponding identity file and recipient."); eprintln!(); eprintln!("When asked below to select an option, use the up/down arrow keys to"); eprintln!("make your choice, or press [Esc] or [q] to quit."); @@ -358,7 +417,7 @@ fn main() -> Result<(), Error> { }) .collect(); - let (stub, recipient, created) = { + let ((stub, recipient, metadata), is_new) = { let (slot_index, slot) = loop { match Select::new() .with_prompt("🕳️ Select a slot for your age identity") @@ -391,13 +450,22 @@ fn main() -> Result<(), Error> { let stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient); let (_, cert) = x509_parser::parse_x509_certificate(key.certificate().as_ref()).unwrap(); - let created = cert.validity().not_before.to_rfc2822(); + let metadata = + util::Metadata::extract(&mut yubikey, slot, &cert, true).unwrap(); - (stub, recipient, created) + ((stub, recipient, metadata), false) } else { return Ok(()); } } else { + let name = Input::::new() + .with_prompt(format!( + "📛 Name this identity [{}]", + flags.name.as_deref().unwrap_or("age identity TAG_HEX") + )) + .allow_empty(true) + .interact_text()?; + let pin_policy = match Select::new() .with_prompt("🔤 Select a PIN policy") .items(&[ @@ -405,7 +473,14 @@ fn main() -> Result<(), Error> { "Once (A PIN is required once per session, if set)", "Never (A PIN is NOT required to decrypt)", ]) - .default(1) + .default( + [PinPolicy::Always, PinPolicy::Once, PinPolicy::Never] + .iter() + .position(|p| { + p == &flags.pin_policy.unwrap_or(builder::DEFAULT_PIN_POLICY) + }) + .unwrap(), + ) .interact_opt()? { Some(0) => PinPolicy::Always, @@ -422,7 +497,13 @@ fn main() -> Result<(), Error> { "Cached (A physical touch is required for decryption, and is cached for 15 seconds)", "Never (A physical touch is NOT required to decrypt)", ]) - .default(0) + .default( + [TouchPolicy::Always, TouchPolicy::Cached, TouchPolicy::Never] + .iter() + .position(|p| p == &flags + .touch_policy.unwrap_or(builder::DEFAULT_TOUCH_POLICY)) + .unwrap(), + ) .interact_opt()? { Some(0) => TouchPolicy::Always, @@ -437,19 +518,99 @@ fn main() -> Result<(), Error> { .interact()? { eprintln!(); - builder::IdentityBuilder::new(Some(slot)) - .with_name(opts.name) - .with_pin_policy(Some(pin_policy)) - .with_touch_policy(Some(touch_policy)) - .force(opts.force) - .build(&mut yubikey)? + ( + builder::IdentityBuilder::new(Some(slot)) + .with_name(match name { + s if s.is_empty() => flags.name, + s => Some(s), + }) + .with_pin_policy(Some(pin_policy)) + .with_touch_policy(Some(touch_policy)) + .build(&mut yubikey)?, + true, + ) } else { return Ok(()); } } }; - util::print_identity(stub, recipient, &created); + eprintln!(); + let file_name = Input::::new() + .with_prompt("📝 File name to write this identity to") + .default(format!( + "age-yubikey-identity-{}.txt", + hex::encode(stub.tag) + )) + .interact_text()?; + + let mut file = match OpenOptions::new() + .create_new(true) + .write(true) + .open(&file_name) + { + Ok(file) => file, + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => { + if Confirm::new() + .with_prompt("File exists. Overwrite it?") + .interact()? + { + File::create(&file_name)? + } else { + return Ok(()); + } + } + Err(e) => return Err(e.into()), + }; + + writeln!(file, "{}", metadata)?; + writeln!(file, "# Recipient: {}", recipient)?; + writeln!(file, "{}", stub.to_string())?; + file.sync_data()?; + + // If `rage` binary is installed, use it in examples. Otherwise default to `age`. + let age_binary = which::which("rage").map(|_| "rage").unwrap_or("age"); + + eprintln!(); + eprintln!("✅ Done! This YubiKey identity is ready to go."); + eprintln!(); + if is_new { + eprintln!("🔑 Here's your shiny new YubiKey recipient:"); + } else { + eprintln!("🔑 Here's the corresponding YubiKey recipient:"); + } + eprintln!(" {}", recipient); + eprintln!(); + eprintln!("Here are some example things you can do with it:"); + eprintln!(); + eprintln!("- Encrypt a file to this identity:"); + eprintln!( + " $ cat foo.txt | {} -r {} -o foo.txt.age", + age_binary, recipient + ); + eprintln!(); + eprintln!("- Decrypt a file with this identity:"); + eprintln!( + " $ cat foo.txt.age | {} -d -i {} > foo.txt", + age_binary, file_name + ); + eprintln!(); + eprintln!("- Recreate the identity file:"); + eprintln!( + " $ age-plugin-yubikey -i --serial {} --slot {} > {}", + stub.serial, + util::slot_to_ui(&stub.slot), + file_name, + ); + eprintln!(); + eprintln!("- Recreate the recipient:"); + eprintln!( + " $ age-plugin-yubikey -l --serial {} --slot {}", + stub.serial, + util::slot_to_ui(&stub.slot), + ); + eprintln!(); + eprintln!("💭 Remember: everything breaks, have a backup plan for when this YubiKey does."); Ok(()) } diff --git a/src/p256.rs b/src/p256.rs index 6054af7..d905cdd 100644 --- a/src/p256.rs +++ b/src/p256.rs @@ -57,7 +57,7 @@ impl Recipient { } pub(crate) fn tag(&self) -> [u8; TAG_BYTES] { - let tag = Sha256::digest(self.to_string().as_bytes()); + let tag = Sha256::digest(self.to_encoded().as_bytes()); (&tag[0..TAG_BYTES]).try_into().expect("length is correct") } diff --git a/src/util.rs b/src/util.rs index f4e8a82..4ec7c4b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,8 +1,10 @@ +use std::fmt; + use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid}; use yubikey_piv::{ - key::RetiredSlotId, + key::{RetiredSlotId, SlotId}, policy::{PinPolicy, TouchPolicy}, - Key, YubiKey, + Serial, YubiKey, }; use crate::{error::Error, p256::Recipient, yubikey::Stub, BINARY_NAME, USABLE_SLOTS}; @@ -89,68 +91,108 @@ pub(crate) fn extract_name(cert: &X509Certificate, all: bool) -> Option<(String, } } -pub(crate) fn extract_name_and_policies( - yubikey: &mut YubiKey, - key: &Key, - cert: &X509Certificate, - all: bool, -) -> Option<(String, Option, Option)> { - // We store the PIN and touch policies for identities in their certificates - // using the same certificate extension as PIV attestations. - // https://developers.yubico.com/PIV/Introduction/PIV_attestation.html - let policies = |c: &X509Certificate| { - c.extensions() - .get(&Oid::from(POLICY_EXTENSION_OID).unwrap()) - // If the encoded extension doesn't have 2 bytes, we assume it is invalid. - .filter(|policy| policy.value.len() >= 2) - .map(|policy| { - // We should only ever see one of three values for either policy, but - // handle unknown values just in case. - let pin_policy = match policy.value[0] { - 0x01 => Some(PinPolicy::Never), - 0x02 => Some(PinPolicy::Once), - 0x03 => Some(PinPolicy::Always), - _ => None, - }; - let touch_policy = match policy.value[1] { - 0x01 => Some(TouchPolicy::Never), - 0x02 => Some(TouchPolicy::Always), - 0x03 => Some(TouchPolicy::Cached), - _ => None, - }; - (pin_policy, touch_policy) - }) - .unwrap_or((None, None)) - }; - - extract_name(cert, all).map(|(name, ours)| { - if ours { - let (pin_policy, touch_policy) = policies(&cert); - (name, pin_policy, touch_policy) - } else { - // We can extract the PIN and touch policies via an attestation. This - // is slow, but the user has asked for all compatible keys, so... - let (pin_policy, touch_policy) = yubikey_piv::key::attest(yubikey, key.slot()) - .ok() - .and_then(|buf| { - x509_parser::parse_x509_certificate(&buf) - .map(|(_, c)| policies(&c)) - .ok() - }) - .unwrap_or((None, None)); - - (name, pin_policy, touch_policy) - } - }) +pub(crate) struct Metadata { + serial: Serial, + slot: RetiredSlotId, + name: String, + created: String, + pin_policy: Option, + touch_policy: Option, } -pub(crate) fn print_identity(stub: Stub, recipient: Recipient, created: &str) { +impl Metadata { + pub(crate) fn extract( + yubikey: &mut YubiKey, + slot: RetiredSlotId, + cert: &X509Certificate, + all: bool, + ) -> Option { + // We store the PIN and touch policies for identities in their certificates + // using the same certificate extension as PIV attestations. + // https://developers.yubico.com/PIV/Introduction/PIV_attestation.html + let policies = |c: &X509Certificate| { + c.extensions() + .get(&Oid::from(POLICY_EXTENSION_OID).unwrap()) + // If the encoded extension doesn't have 2 bytes, we assume it is invalid. + .filter(|policy| policy.value.len() >= 2) + .map(|policy| { + // We should only ever see one of three values for either policy, but + // handle unknown values just in case. + let pin_policy = match policy.value[0] { + 0x01 => Some(PinPolicy::Never), + 0x02 => Some(PinPolicy::Once), + 0x03 => Some(PinPolicy::Always), + _ => None, + }; + let touch_policy = match policy.value[1] { + 0x01 => Some(TouchPolicy::Never), + 0x02 => Some(TouchPolicy::Always), + 0x03 => Some(TouchPolicy::Cached), + _ => None, + }; + (pin_policy, touch_policy) + }) + .unwrap_or((None, None)) + }; + + extract_name(cert, all) + .map(|(name, ours)| { + if ours { + let (pin_policy, touch_policy) = policies(&cert); + (name, pin_policy, touch_policy) + } else { + // We can extract the PIN and touch policies via an attestation. This + // is slow, but the user has asked for all compatible keys, so... + let (pin_policy, touch_policy) = + yubikey_piv::key::attest(yubikey, SlotId::Retired(slot)) + .ok() + .and_then(|buf| { + x509_parser::parse_x509_certificate(&buf) + .map(|(_, c)| policies(&c)) + .ok() + }) + .unwrap_or((None, None)); + + (name, pin_policy, touch_policy) + } + }) + .map(|(name, pin_policy, touch_policy)| Metadata { + serial: yubikey.serial(), + slot, + name, + created: cert.validity().not_before.to_rfc2822(), + pin_policy, + touch_policy, + }) + } +} + +impl fmt::Display for Metadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!( + f, + "# Serial: {}, Slot: {}", + self.serial, + slot_to_ui(&self.slot) + )?; + writeln!(f, "# Name: {}", self.name)?; + writeln!(f, "# Created: {}", self.created)?; + writeln!(f, "# PIN policy: {}", pin_policy_to_str(self.pin_policy))?; + write!( + f, + "# Touch policy: {}", + touch_policy_to_str(self.touch_policy) + ) + } +} + +pub(crate) fn print_identity(stub: Stub, recipient: Recipient, metadata: Metadata) { let recipient = recipient.to_string(); if !console::user_attended() { eprintln!("Recipient: {}", recipient); } - println!("# created: {}", created); - println!("# recipient: {}", recipient); + println!("{}", metadata); + println!("# Recipient: {}", recipient); println!("{}", stub.to_string()); }