Merge pull request #33 from str4d/final-changes

Final changes before 0.1.0
This commit is contained in:
str4d
2021-05-01 22:52:07 +01:00
committed by GitHub
9 changed files with 401 additions and 175 deletions
Generated
+17
View File
@@ -64,6 +64,7 @@ dependencies = [
"rand 0.7.3", "rand 0.7.3",
"secrecy", "secrecy",
"sha2", "sha2",
"which",
"x509", "x509",
"x509-parser", "x509-parser",
"yubikey-piv", "yubikey-piv",
@@ -353,6 +354,12 @@ dependencies = [
"signature", "signature",
] ]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]] [[package]]
name = "elliptic-curve" name = "elliptic-curve"
version = "0.8.5" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
+1
View File
@@ -37,6 +37,7 @@ pcsc = "2.4"
rand = "0.7" rand = "0.7"
secrecy = "0.7" secrecy = "0.7"
sha2 = "0.9" sha2 = "0.9"
which = "4.1"
x509 = "0.2" x509 = "0.2"
x509-parser = "0.9" x509-parser = "0.9"
yubikey-piv = { version = "0.3", features = ["untested"] } yubikey-piv = { version = "0.3", features = ["untested"] }
+3 -3
View File
@@ -49,18 +49,18 @@ fn main() {
Flag::new() Flag::new()
.short("-i") .short("-i")
.long("--identity") .long("--identity")
.help("Print the identity stored in a YubiKey slot."), .help("Print identities stored in connected YubiKeys."),
) )
.flag( .flag(
Flag::new() Flag::new()
.short("-l") .short("-l")
.long("--list") .long("--list")
.help("List all age identities in connected YubiKeys."), .help("List recipients for age identities in connected YubiKeys."),
) )
.flag( .flag(
Flag::new() Flag::new()
.long("--list-all") .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(
Flag::new() Flag::new()
+6 -6
View File
@@ -10,13 +10,13 @@ use yubikey_piv::{
use crate::{ use crate::{
error::Error, error::Error,
p256::Recipient, p256::Recipient,
util::POLICY_EXTENSION_OID, util::{Metadata, POLICY_EXTENSION_OID},
yubikey::{self, Stub}, yubikey::{self, Stub},
BINARY_NAME, USABLE_SLOTS, BINARY_NAME, USABLE_SLOTS,
}; };
const DEFAULT_PIN_POLICY: PinPolicy = PinPolicy::Once; pub(crate) const DEFAULT_PIN_POLICY: PinPolicy = PinPolicy::Once;
const DEFAULT_TOUCH_POLICY: TouchPolicy = TouchPolicy::Always; pub(crate) const DEFAULT_TOUCH_POLICY: TouchPolicy = TouchPolicy::Always;
pub(crate) struct IdentityBuilder { pub(crate) struct IdentityBuilder {
slot: Option<RetiredSlotId>, slot: Option<RetiredSlotId>,
@@ -57,7 +57,7 @@ impl IdentityBuilder {
self 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 { let slot = match self.slot {
Some(slot) => { Some(slot) => {
if !self.force { if !self.force {
@@ -141,12 +141,12 @@ impl IdentityBuilder {
)?; )?;
let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref()).unwrap(); 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(( Ok((
Stub::new(yubikey.serial(), slot, &recipient), Stub::new(yubikey.serial(), slot, &recipient),
recipient, recipient,
created, metadata,
)) ))
} }
} }
+14 -9
View File
@@ -6,20 +6,21 @@ use crate::util::slot_to_ui;
pub enum Error { pub enum Error {
CustomManagementKey, CustomManagementKey,
InvalidFlagCommand(String, String),
InvalidFlagTui(String),
InvalidPinLength, InvalidPinLength,
InvalidPinPolicy(String), InvalidPinPolicy(String),
InvalidSlot(u8), InvalidSlot(u8),
InvalidTouchPolicy(String), InvalidTouchPolicy(String),
Io(io::Error), Io(io::Error),
MultipleCommands, MultipleCommands,
MultipleIdentities,
MultipleYubiKeys, MultipleYubiKeys,
NoEmptySlots(Serial), NoEmptySlots(Serial),
NoIdentities,
NoMatchingSerial(Serial), NoMatchingSerial(Serial),
SlotHasNoIdentity(RetiredSlotId), SlotHasNoIdentity(RetiredSlotId),
SlotIsNotEmpty(RetiredSlotId), SlotIsNotEmpty(RetiredSlotId),
TimedOut, TimedOut,
UseListForSingleSlot,
YubiKey(yubikey_piv::Error), YubiKey(yubikey_piv::Error),
} }
@@ -43,6 +44,14 @@ impl fmt::Debug for Error {
Error::CustomManagementKey => { Error::CustomManagementKey => {
writeln!(f, "Custom unprotected management keys are not supported.")? 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::InvalidPinLength => writeln!(f, "The PIN needs to be 1-8 characters.")?,
Error::InvalidPinPolicy(s) => writeln!( Error::InvalidPinPolicy(s) => writeln!(
f, f,
@@ -64,10 +73,6 @@ impl fmt::Debug for Error {
f, f,
"Only one of --generate, --identity, --list, --list-all can be specified." "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!( Error::MultipleYubiKeys => writeln!(
f, f,
"Multiple YubiKeys are plugged in. Use --serial to select a single YubiKey." "Multiple YubiKeys are plugged in. Use --serial to select a single YubiKey."
@@ -75,9 +80,6 @@ impl fmt::Debug for Error {
Error::NoEmptySlots(serial) => { Error::NoEmptySlots(serial) => {
writeln!(f, "YubiKey with serial {} has no empty slots.", 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) => { Error::NoMatchingSerial(serial) => {
writeln!(f, "Could not find YubiKey with serial {}.", serial)? writeln!(f, "Could not find YubiKey with serial {}.", serial)?
} }
@@ -94,6 +96,9 @@ impl fmt::Debug for Error {
Error::TimedOut => { Error::TimedOut => {
writeln!(f, "Timed out while waiting for a YubiKey to be inserted.")? 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 { Error::YubiKey(e) => match e {
yubikey_piv::error::Error::NotFound => { yubikey_piv::error::Error::NotFound => {
writeln!(f, "Please insert the YubiKey you want to set up")? writeln!(f, "Please insert the YubiKey you want to set up")?
+1 -1
View File
@@ -9,7 +9,7 @@ use std::convert::TryInto;
use crate::{p256::Recipient, STANZA_TAG}; 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 TAG_BYTES: usize = 4;
const EPK_BYTES: usize = 33; const EPK_BYTES: usize = 33;
+258 -97
View File
@@ -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 age_plugin::run_state_machine;
use dialoguer::{Confirm, Select}; use dialoguer::{Confirm, Input, Select};
use gumdrop::Options; use gumdrop::Options;
use yubikey_piv::{ use yubikey_piv::{
certificate::PublicKeyInfo, certificate::PublicKeyInfo,
key::{RetiredSlotId, SlotId}, key::{RetiredSlotId, SlotId},
policy::{PinPolicy, TouchPolicy}, policy::{PinPolicy, TouchPolicy},
Key, Readers, Key, Readers, Serial,
}; };
mod builder; mod builder;
@@ -68,13 +72,16 @@ struct PluginOptions {
#[options(help = "Generate a new YubiKey identity.")] #[options(help = "Generate a new YubiKey identity.")]
generate: bool, generate: bool,
#[options(help = "Print the identity stored in a YubiKey slot.")] #[options(help = "Print identities stored in connected YubiKeys.")]
identity: bool, identity: bool,
#[options(help = "List all age identities in connected YubiKeys.")] #[options(help = "List recipients for age identities in connected YubiKeys.")]
list: bool, 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, list_all: bool,
#[options( #[options(
@@ -105,36 +112,61 @@ struct PluginOptions {
touch_policy: Option<String>, touch_policy: Option<String>,
} }
fn generate(opts: PluginOptions) -> Result<(), Error> { struct PluginFlags {
let serial = opts.serial.map(|s| s.into()); serial: Option<Serial>,
let slot = opts.slot.map(util::ui_to_slot).transpose()?; slot: Option<RetiredSlotId>,
let pin_policy = opts name: Option<String>,
.pin_policy pin_policy: Option<PinPolicy>,
.map(util::pin_policy_from_string) touch_policy: Option<TouchPolicy>,
.transpose()?; force: bool,
let touch_policy = opts }
.touch_policy
.map(util::touch_policy_from_string)
.transpose()?;
let mut yubikey = yubikey::open(serial)?; impl TryFrom<PluginOptions> for PluginFlags {
type Error = Error;
let (stub, recipient, created) = builder::IdentityBuilder::new(slot) fn try_from(opts: PluginOptions) -> Result<Self, Self::Error> {
.with_name(opts.name) let serial = opts.serial.map(|s| s.into());
.with_pin_policy(pin_policy) let slot = opts.slot.map(util::ui_to_slot).transpose()?;
.with_touch_policy(touch_policy) let pin_policy = opts
.force(opts.force) .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)?; .build(&mut yubikey)?;
util::print_identity(stub, recipient, &created); util::print_identity(stub, recipient, metadata);
Ok(()) Ok(())
} }
fn identity(opts: PluginOptions) -> Result<(), Error> { fn print_single(
let serial = opts.serial.map(|s| s.into()); serial: Option<Serial>,
let slot = opts.slot.map(util::ui_to_slot).transpose()?; slot: RetiredSlotId,
printer: impl Fn(yubikey::Stub, p256::Recipient, util::Metadata),
) -> Result<(), Error> {
let mut yubikey = yubikey::open(serial)?; let mut yubikey = yubikey::open(serial)?;
let mut keys = Key::list(&mut yubikey)?.into_iter().filter_map(|key| { 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 { let (key, slot, recipient) = keys
keys.find(|(_, s, _)| s == &slot) .find(|(_, s, _)| s == &slot)
.ok_or(Error::SlotHasNoIdentity(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 stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient); 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() .ok()
.map(|(_, cert)| cert.validity().not_before.to_rfc2822()) .and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, true))
.unwrap_or_else(|| "Unknown".to_owned()); .unwrap();
util::print_identity(stub, recipient, &created); printer(stub, recipient, metadata);
Ok(()) Ok(())
} }
fn list(all: bool) -> Result<(), Error> { fn print_multiple(
kind: &str,
serial: Option<Serial>,
all: bool,
printer: impl Fn(yubikey::Stub, p256::Recipient, util::Metadata),
) -> Result<(), Error> {
let mut readers = Readers::open()?; let mut readers = Readers::open()?;
let mut printed = 0;
for reader in readers.iter()?.filter(yubikey::filter_connected) { for reader in readers.iter()?.filter(yubikey::filter_connected) {
let mut yubikey = reader.open()?; let mut yubikey = reader.open()?;
if let Some(serial) = serial {
if yubikey.serial() != serial {
continue;
}
}
for key in Key::list(&mut yubikey)? { for key in Key::list(&mut yubikey)? {
// We only use the retired slots. // We only use the retired slots.
@@ -205,38 +228,71 @@ fn list(all: bool) -> Result<(), Error> {
_ => continue, _ => continue,
}; };
let ((name, pin_policy, touch_policy), created) = let stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient);
match x509_parser::parse_x509_certificate(key.certificate().as_ref()) let metadata = match x509_parser::parse_x509_certificate(key.certificate().as_ref())
.ok() .ok()
.and_then(|(_, cert)| { .and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, all))
util::extract_name_and_policies(&mut yubikey, &key, &cert, all) {
.map(|res| (res, cert.validity().not_before.to_rfc2822())) Some(res) => res,
}) { None => continue,
Some(res) => res, };
None => continue,
};
println!( printer(stub, recipient, metadata);
"# Serial: {}, Slot: {}", printed += 1;
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());
println!(); println!();
} }
println!(); println!();
} }
if printed > 1 {
eprintln!(
"Generated {} for {} slots. If you intended to select a slot, use --slot.",
kind, printed,
);
}
Ok(()) 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> { fn main() -> Result<(), Error> {
env_logger::builder() env_logger::builder()
.format_timestamp(None) .format_timestamp(None)
@@ -266,14 +322,19 @@ fn main() -> Result<(), Error> {
println!("age-plugin-yubikey {}", env!("CARGO_PKG_VERSION")); println!("age-plugin-yubikey {}", env!("CARGO_PKG_VERSION"));
Ok(()) Ok(())
} else if opts.generate { } else if opts.generate {
generate(opts) generate(opts.try_into()?)
} else if opts.identity { } else if opts.identity {
identity(opts) identity(opts.try_into()?)
} else if opts.list { } else if opts.list {
list(false) list(opts.try_into()?, false)
} else if opts.list_all { } else if opts.list_all {
list(true) list(opts.try_into()?, true)
} else { } 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!("✨ Let's get your YubiKey set up for age! ✨");
eprintln!(); eprintln!();
eprintln!("This tool can create a new age identity in a free slot of your YubiKey."); 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!(" age-plugin-yubikey --generate");
eprintln!(); eprintln!();
eprintln!("If you are already using a YubiKey with age, you can select an existing"); 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!("slot to recreate its corresponding identity file and recipient.");
eprintln!("also obtain this directly with:");
eprintln!(" age-plugin-yubikey --identity");
eprintln!(); eprintln!();
eprintln!("When asked below to select an option, use the up/down arrow keys to"); 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."); eprintln!("make your choice, or press [Esc] or [q] to quit.");
@@ -358,7 +417,7 @@ fn main() -> Result<(), Error> {
}) })
.collect(); .collect();
let (stub, recipient, created) = { let ((stub, recipient, metadata), is_new) = {
let (slot_index, slot) = loop { let (slot_index, slot) = loop {
match Select::new() match Select::new()
.with_prompt("🕳️ Select a slot for your age identity") .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 stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient);
let (_, cert) = let (_, cert) =
x509_parser::parse_x509_certificate(key.certificate().as_ref()).unwrap(); 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 { } else {
return Ok(()); return Ok(());
} }
} else { } else {
let name = Input::<String>::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() let pin_policy = match Select::new()
.with_prompt("🔤 Select a PIN policy") .with_prompt("🔤 Select a PIN policy")
.items(&[ .items(&[
@@ -405,7 +473,14 @@ fn main() -> Result<(), Error> {
"Once (A PIN is required once per session, if set)", "Once (A PIN is required once per session, if set)",
"Never (A PIN is NOT required to decrypt)", "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()? .interact_opt()?
{ {
Some(0) => PinPolicy::Always, 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)", "Cached (A physical touch is required for decryption, and is cached for 15 seconds)",
"Never (A physical touch is NOT required to decrypt)", "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()? .interact_opt()?
{ {
Some(0) => TouchPolicy::Always, Some(0) => TouchPolicy::Always,
@@ -437,19 +518,99 @@ fn main() -> Result<(), Error> {
.interact()? .interact()?
{ {
eprintln!(); eprintln!();
builder::IdentityBuilder::new(Some(slot)) (
.with_name(opts.name) builder::IdentityBuilder::new(Some(slot))
.with_pin_policy(Some(pin_policy)) .with_name(match name {
.with_touch_policy(Some(touch_policy)) s if s.is_empty() => flags.name,
.force(opts.force) s => Some(s),
.build(&mut yubikey)? })
.with_pin_policy(Some(pin_policy))
.with_touch_policy(Some(touch_policy))
.build(&mut yubikey)?,
true,
)
} else { } else {
return Ok(()); return Ok(());
} }
} }
}; };
util::print_identity(stub, recipient, &created); eprintln!();
let file_name = Input::<String>::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(()) Ok(())
} }
+1 -1
View File
@@ -57,7 +57,7 @@ impl Recipient {
} }
pub(crate) fn tag(&self) -> [u8; TAG_BYTES] { 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") (&tag[0..TAG_BYTES]).try_into().expect("length is correct")
} }
+100 -58
View File
@@ -1,8 +1,10 @@
use std::fmt;
use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid}; use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid};
use yubikey_piv::{ use yubikey_piv::{
key::RetiredSlotId, key::{RetiredSlotId, SlotId},
policy::{PinPolicy, TouchPolicy}, policy::{PinPolicy, TouchPolicy},
Key, YubiKey, Serial, YubiKey,
}; };
use crate::{error::Error, p256::Recipient, yubikey::Stub, BINARY_NAME, USABLE_SLOTS}; 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( pub(crate) struct Metadata {
yubikey: &mut YubiKey, serial: Serial,
key: &Key, slot: RetiredSlotId,
cert: &X509Certificate, name: String,
all: bool, created: String,
) -> Option<(String, Option<PinPolicy>, Option<TouchPolicy>)> { pin_policy: Option<PinPolicy>,
// We store the PIN and touch policies for identities in their certificates touch_policy: Option<TouchPolicy>,
// 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) 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<Self> {
// 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(); let recipient = recipient.to_string();
if !console::user_attended() { if !console::user_attended() {
eprintln!("Recipient: {}", recipient); eprintln!("Recipient: {}", recipient);
} }
println!("# created: {}", created); println!("{}", metadata);
println!("# recipient: {}", recipient); println!("# Recipient: {}", recipient);
println!("{}", stub.to_string()); println!("{}", stub.to_string());
} }