From 6da29e71257b5c50870a07acc985ffa23e0fe610 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 23 Apr 2021 22:56:43 +1200 Subject: [PATCH 01/14] Remove URL prefix from HKDF label Closes str4d/age-plugin-yubikey#31. --- src/format.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 72c5278de00b048d29cdd4ce605d7e62574ec578 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 23 Apr 2021 23:01:08 +1200 Subject: [PATCH 02/14] Hash public key directly for stanza tag Closes str4d/age-plugin-yubikey#32. --- src/p256.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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") } From 64b0ab4e16ae602ac2d75a1b40fdca9f6cde60f0 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 25 Apr 2021 12:10:35 +1200 Subject: [PATCH 03/14] Add --list comments to identity output This improves the output of --generate and --identity, as well as the interactive TUI. Closes str4d/age-plugin-yubikey#24. --- src/builder.rs | 8 +-- src/main.rs | 51 ++++++---------- src/util.rs | 158 +++++++++++++++++++++++++++++++------------------ 3 files changed, 123 insertions(+), 94 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index eab165b..3890136 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -10,7 +10,7 @@ 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, }; @@ -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/main.rs b/src/main.rs index 697387e..20fd5e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,14 +119,14 @@ fn generate(opts: PluginOptions) -> Result<(), Error> { let mut yubikey = yubikey::open(serial)?; - let (stub, recipient, created) = builder::IdentityBuilder::new(slot) + let (stub, recipient, metadata) = builder::IdentityBuilder::new(slot) .with_name(opts.name) .with_pin_policy(pin_policy) .with_touch_policy(touch_policy) .force(opts.force) .build(&mut yubikey)?; - util::print_identity(stub, recipient, &created); + util::print_identity(stub, recipient, metadata); Ok(()) } @@ -173,12 +173,12 @@ fn identity(opts: PluginOptions) -> Result<(), Error> { }?; 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); + util::print_identity(stub, recipient, metadata); Ok(()) } @@ -205,29 +205,15 @@ 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 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!("{}", metadata); println!("{}", recipient.to_string()); println!(); } @@ -358,7 +344,7 @@ fn main() -> Result<(), Error> { }) .collect(); - let (stub, recipient, created) = { + let (stub, recipient, metadata) = { let (slot_index, slot) = loop { match Select::new() .with_prompt("🕳️ Select a slot for your age identity") @@ -391,9 +377,10 @@ 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) } else { return Ok(()); } @@ -449,7 +436,7 @@ fn main() -> Result<(), Error> { } }; - util::print_identity(stub, recipient, &created); + util::print_identity(stub, recipient, metadata); Ok(()) } 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()); } From d4eae4d631c02f155ff58109cb79a49f62b028ab Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 26 Apr 2021 17:26:10 +1200 Subject: [PATCH 04/14] Add name field to TUI Closes str4d/age-plugin-yubikey#28. --- src/main.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 20fd5e8..2f8e310 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use age_plugin::run_state_machine; -use dialoguer::{Confirm, Select}; +use dialoguer::{Confirm, Input, Select}; use gumdrop::Options; use yubikey_piv::{ certificate::PublicKeyInfo, @@ -385,6 +385,18 @@ fn main() -> Result<(), Error> { return Ok(()); } } else { + let name = match Input::::new() + .with_prompt(format!( + "📛 Name this identity [{}]", + opts.name.as_deref().unwrap_or("age identity TAG_HEX") + )) + .allow_empty(true) + .interact_text()? + { + s if s.is_empty() => opts.name, + s => Some(s), + }; + let pin_policy = match Select::new() .with_prompt("🔤 Select a PIN policy") .items(&[ @@ -425,7 +437,7 @@ fn main() -> Result<(), Error> { { eprintln!(); builder::IdentityBuilder::new(Some(slot)) - .with_name(opts.name) + .with_name(name) .with_pin_policy(Some(pin_policy)) .with_touch_policy(Some(touch_policy)) .force(opts.force) From d9b4fba546b8c29663dbd64f1ccea4c3a29fc13c Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 26 Apr 2021 18:09:31 +1200 Subject: [PATCH 05/14] Collect flag-parsing logic into PluginFlags struct --- src/main.rs | 75 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2f8e310..7b26c50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use std::convert::{TryFrom, TryInto}; + use age_plugin::run_state_machine; use dialoguer::{Confirm, Input, Select}; use gumdrop::Options; @@ -5,7 +7,7 @@ use yubikey_piv::{ certificate::PublicKeyInfo, key::{RetiredSlotId, SlotId}, policy::{PinPolicy, TouchPolicy}, - Key, Readers, + Key, Readers, Serial, }; mod builder; @@ -105,25 +107,49 @@ 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, metadata) = 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, metadata); @@ -131,11 +157,8 @@ fn generate(opts: PluginOptions) -> Result<(), Error> { 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()?; - - let mut yubikey = yubikey::open(serial)?; +fn identity(flags: PluginFlags) -> Result<(), Error> { + let mut yubikey = yubikey::open(flags.serial)?; let mut keys = Key::list(&mut yubikey)?.into_iter().filter_map(|key| { // - We only use the retired slots. @@ -148,7 +171,7 @@ fn identity(opts: PluginOptions) -> Result<(), Error> { } }); - let (key, slot, recipient) = if let Some(slot) = slot { + let (key, slot, recipient) = if let Some(slot) = flags.slot { keys.find(|(_, s, _)| s == &slot) .ok_or(Error::SlotHasNoIdentity(slot)) } else { @@ -252,9 +275,9 @@ 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) } else if opts.list_all { From acdbb790839c1b9291008d9112b258ed1a26a82d Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 26 Apr 2021 18:11:33 +1200 Subject: [PATCH 06/14] Use non-short flags as defaults for TUI Closes str4d/age-plugin-yubikey#27. --- src/builder.rs | 4 ++-- src/main.rs | 36 +++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 3890136..aaf1538 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -15,8 +15,8 @@ use crate::{ 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, diff --git a/src/main.rs b/src/main.rs index 7b26c50..e26493b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -283,6 +283,8 @@ fn main() -> Result<(), Error> { } else if opts.list_all { list(true) } else { + 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."); @@ -408,17 +410,13 @@ fn main() -> Result<(), Error> { return Ok(()); } } else { - let name = match Input::::new() + let name = Input::::new() .with_prompt(format!( "📛 Name this identity [{}]", - opts.name.as_deref().unwrap_or("age identity TAG_HEX") + flags.name.as_deref().unwrap_or("age identity TAG_HEX") )) .allow_empty(true) - .interact_text()? - { - s if s.is_empty() => opts.name, - s => Some(s), - }; + .interact_text()?; let pin_policy = match Select::new() .with_prompt("🔤 Select a PIN policy") @@ -427,7 +425,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, @@ -444,7 +449,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, @@ -460,10 +471,13 @@ fn main() -> Result<(), Error> { { eprintln!(); builder::IdentityBuilder::new(Some(slot)) - .with_name(name) + .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)) - .force(opts.force) + .force(flags.force) .build(&mut yubikey)? } else { return Ok(()); From c230d93726c4f3c6c0b864732443a0c5e85a88cf Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 26 Apr 2021 22:21:23 +1200 Subject: [PATCH 07/14] TUI: Write identity to file Closes str4d/age-plugin-yubikey#23. --- src/main.rs | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index e26493b..c05a624 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ use std::convert::{TryFrom, TryInto}; +use std::fs::{File, OpenOptions}; +use std::io::{self, Write}; use age_plugin::run_state_machine; use dialoguer::{Confirm, Input, Select}; @@ -485,7 +487,40 @@ fn main() -> Result<(), Error> { } }; - util::print_identity(stub, recipient, metadata); + 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())?; + + eprintln!(); + eprintln!("💭 Remember: everything breaks, have a backup plan for when this YubiKey does."); Ok(()) } From 2a013fc0184a3921b5d07cc2f43756d750d6af16 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 26 Apr 2021 23:13:05 +1200 Subject: [PATCH 08/14] Add example commands to end of TUI Closes str4d/age-plugin-yubikey#25. --- src/main.rs | 50 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index c05a624..06f0aa1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -371,7 +371,7 @@ fn main() -> Result<(), Error> { }) .collect(); - let (stub, recipient, metadata) = { + let ((stub, recipient, metadata), is_new) = { let (slot_index, slot) = loop { match Select::new() .with_prompt("🕳️ Select a slot for your age identity") @@ -407,7 +407,7 @@ fn main() -> Result<(), Error> { let metadata = util::Metadata::extract(&mut yubikey, slot, &cert, true).unwrap(); - (stub, recipient, metadata) + ((stub, recipient, metadata), false) } else { return Ok(()); } @@ -472,15 +472,18 @@ fn main() -> Result<(), Error> { .interact()? { eprintln!(); - 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)) - .force(flags.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)) + .force(flags.force) + .build(&mut yubikey)?, + true, + ) } else { return Ok(()); } @@ -519,6 +522,31 @@ fn main() -> Result<(), Error> { writeln!(file, "# Recipient: {}", recipient)?; writeln!(file, "{}", stub.to_string())?; + 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 | age -r {} -o foo.txt.age", recipient); + eprintln!(); + eprintln!("- Decrypt a file with this identity:"); + eprintln!(" $ cat foo.txt.age | age -d -i {} > foo.txt", 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!("💭 Remember: everything breaks, have a backup plan for when this YubiKey does."); From 458a09125fe6889661b485aed658d6746605dd59 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 29 Apr 2021 00:04:37 +1200 Subject: [PATCH 09/14] Make --list{-all} and --identity behave the same way - Both commands print all slots from all connected YubiKeys by default. - If --serial is specified (without --slot) then print all slots in that YubiKey. - If --slot is specified then then instead print a single slot (requiring --serial if necessary to select a single YubiKey). Closes str4d/age-plugin-yubikey#26. --- examples/generate-docs.rs | 6 +-- src/error.rs | 9 ---- src/main.rs | 104 +++++++++++++++++++++++++------------- 3 files changed, 73 insertions(+), 46 deletions(-) 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/error.rs b/src/error.rs index 2f5ed81..c71ba65 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,10 +12,8 @@ pub enum Error { InvalidTouchPolicy(String), Io(io::Error), MultipleCommands, - MultipleIdentities, MultipleYubiKeys, NoEmptySlots(Serial), - NoIdentities, NoMatchingSerial(Serial), SlotHasNoIdentity(RetiredSlotId), SlotIsNotEmpty(RetiredSlotId), @@ -64,10 +62,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 +69,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)? } diff --git a/src/main.rs b/src/main.rs index 06f0aa1..0e23b99 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,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( @@ -159,8 +162,12 @@ fn generate(flags: PluginFlags) -> Result<(), Error> { Ok(()) } -fn identity(flags: PluginFlags) -> Result<(), Error> { - let mut yubikey = yubikey::open(flags.serial)?; +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| { // - We only use the retired slots. @@ -173,29 +180,9 @@ fn identity(flags: PluginFlags) -> Result<(), Error> { } }); - let (key, slot, recipient) = if let Some(slot) = flags.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 metadata = x509_parser::parse_x509_certificate(key.certificate().as_ref()) @@ -203,16 +190,27 @@ fn identity(flags: PluginFlags) -> Result<(), Error> { .and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, true)) .unwrap(); - util::print_identity(stub, recipient, metadata); + 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. @@ -230,6 +228,7 @@ fn list(all: bool) -> Result<(), Error> { _ => 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)) @@ -238,16 +237,46 @@ fn list(all: bool) -> Result<(), Error> { None => continue, }; - println!("{}", metadata); - 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> { + print_details("identities", flags, false, util::print_identity) +} + +fn list(flags: PluginFlags, all: bool) -> Result<(), Error> { + print_details("recipients", flags, all, |_, recipient, metadata| { + println!("{}", metadata); + println!("{}", recipient.to_string()); + }) +} + fn main() -> Result<(), Error> { env_logger::builder() .format_timestamp(None) @@ -281,9 +310,9 @@ fn main() -> Result<(), Error> { } else if opts.identity { 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 { let flags: PluginFlags = opts.try_into()?; @@ -548,6 +577,13 @@ fn main() -> Result<(), Error> { 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(()) From ba3ccda8ce8a571814d82353f2ba55b99a306281 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 2 May 2021 09:23:58 +1200 Subject: [PATCH 10/14] TUI: Use rage binary in example commands if installed --- Cargo.lock | 17 +++++++++++++++++ Cargo.toml | 1 + src/main.rs | 13 +++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) 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/src/main.rs b/src/main.rs index 0e23b99..038086e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -551,6 +551,9 @@ fn main() -> Result<(), Error> { writeln!(file, "# Recipient: {}", recipient)?; writeln!(file, "{}", stub.to_string())?; + // 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!(); @@ -564,10 +567,16 @@ fn main() -> Result<(), Error> { eprintln!("Here are some example things you can do with it:"); eprintln!(); eprintln!("- Encrypt a file to this identity:"); - eprintln!(" $ cat foo.txt | age -r {} -o foo.txt.age", recipient); + 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 | age -d -i {} > foo.txt", file_name); + eprintln!( + " $ cat foo.txt.age | {} -d -i {} > foo.txt", + age_binary, file_name + ); eprintln!(); eprintln!("- Recreate the identity file:"); eprintln!( From 9276725a9af534532307f3046d18bc7a2dbaaf78 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 2 May 2021 09:24:16 +1200 Subject: [PATCH 11/14] TUI: Ensure that identity file data is written --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 038086e..b919f5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -550,6 +550,7 @@ fn main() -> Result<(), Error> { 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"); From bf58ee9b82657f2c8d96c4e0b4f86b785cd4b8a7 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 2 May 2021 09:24:31 +1200 Subject: [PATCH 12/14] TUI: Remove redundant example --- src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index b919f5e..3089430 100644 --- a/src/main.rs +++ b/src/main.rs @@ -325,9 +325,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."); From 65082edf2238d962d83b3746fd2adc869b86084e Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 2 May 2021 09:31:35 +1200 Subject: [PATCH 13/14] Prevent --slot from being used with --list-all --- src/error.rs | 4 ++++ src/main.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/error.rs b/src/error.rs index c71ba65..d0c535d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,6 +18,7 @@ pub enum Error { SlotHasNoIdentity(RetiredSlotId), SlotIsNotEmpty(RetiredSlotId), TimedOut, + UseListForSingleSlot, YubiKey(yubikey_piv::Error), } @@ -85,6 +86,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/main.rs b/src/main.rs index 3089430..ef320e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -271,6 +271,10 @@ fn identity(flags: PluginFlags) -> Result<(), Error> { } fn list(flags: PluginFlags, all: bool) -> Result<(), Error> { + if all && flags.slot.is_some() { + return Err(Error::UseListForSingleSlot); + } + print_details("recipients", flags, all, |_, recipient, metadata| { println!("{}", metadata); println!("{}", recipient.to_string()); From d36da3fe2d7e0f05e3409c55fa45fb9c5fc96f61 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 2 May 2021 09:38:31 +1200 Subject: [PATCH 14/14] Prevent --force from being used with the TUI or unexpected commands We may want to use --force with some of these later, so we shouldn't allow it to be a no-op at present. --- src/error.rs | 10 ++++++++++ src/main.rs | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index d0c535d..05d0ffb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,6 +6,8 @@ use crate::util::slot_to_ui; pub enum Error { CustomManagementKey, + InvalidFlagCommand(String, String), + InvalidFlagTui(String), InvalidPinLength, InvalidPinPolicy(String), InvalidSlot(u8), @@ -42,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, diff --git a/src/main.rs b/src/main.rs index ef320e4..d2ed3be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -267,6 +267,12 @@ fn print_details( } 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) } @@ -274,6 +280,12 @@ 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); @@ -318,6 +330,9 @@ fn main() -> Result<(), Error> { } else if opts.list_all { 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! ✨"); @@ -511,7 +526,6 @@ fn main() -> Result<(), Error> { }) .with_pin_policy(Some(pin_policy)) .with_touch_policy(Some(touch_policy)) - .force(flags.force) .build(&mut yubikey)?, true, )