From 64b0ab4e16ae602ac2d75a1b40fdca9f6cde60f0 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 25 Apr 2021 12:10:35 +1200 Subject: [PATCH] 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()); }