Merge pull request #33 from str4d/final-changes
Final changes before 0.1.0
This commit is contained in:
Generated
+17
@@ -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"
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
+241
-80
@@ -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,7 +112,19 @@ struct PluginOptions {
|
|||||||
touch_policy: Option<String>,
|
touch_policy: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate(opts: PluginOptions) -> Result<(), Error> {
|
struct PluginFlags {
|
||||||
|
serial: Option<Serial>,
|
||||||
|
slot: Option<RetiredSlotId>,
|
||||||
|
name: Option<String>,
|
||||||
|
pin_policy: Option<PinPolicy>,
|
||||||
|
touch_policy: Option<TouchPolicy>,
|
||||||
|
force: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<PluginOptions> for PluginFlags {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(opts: PluginOptions) -> Result<Self, Self::Error> {
|
||||||
let serial = opts.serial.map(|s| s.into());
|
let serial = opts.serial.map(|s| s.into());
|
||||||
let slot = opts.slot.map(util::ui_to_slot).transpose()?;
|
let slot = opts.slot.map(util::ui_to_slot).transpose()?;
|
||||||
let pin_policy = opts
|
let pin_policy = opts
|
||||||
@@ -117,24 +136,37 @@ fn generate(opts: PluginOptions) -> Result<(), Error> {
|
|||||||
.map(util::touch_policy_from_string)
|
.map(util::touch_policy_from_string)
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
let mut yubikey = yubikey::open(serial)?;
|
Ok(PluginFlags {
|
||||||
|
serial,
|
||||||
|
slot,
|
||||||
|
name: opts.name,
|
||||||
|
pin_policy,
|
||||||
|
touch_policy,
|
||||||
|
force: opts.force,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (stub, recipient, created) = builder::IdentityBuilder::new(slot)
|
fn generate(flags: PluginFlags) -> Result<(), Error> {
|
||||||
.with_name(opts.name)
|
let mut yubikey = yubikey::open(flags.serial)?;
|
||||||
.with_pin_policy(pin_policy)
|
|
||||||
.with_touch_policy(touch_policy)
|
let (stub, recipient, metadata) = builder::IdentityBuilder::new(flags.slot)
|
||||||
.force(opts.force)
|
.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,
|
Some(res) => res,
|
||||||
None => continue,
|
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))
|
builder::IdentityBuilder::new(Some(slot))
|
||||||
.with_name(opts.name)
|
.with_name(match name {
|
||||||
|
s if s.is_empty() => flags.name,
|
||||||
|
s => Some(s),
|
||||||
|
})
|
||||||
.with_pin_policy(Some(pin_policy))
|
.with_pin_policy(Some(pin_policy))
|
||||||
.with_touch_policy(Some(touch_policy))
|
.with_touch_policy(Some(touch_policy))
|
||||||
.force(opts.force)
|
.build(&mut yubikey)?,
|
||||||
.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
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+52
-10
@@ -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,12 +91,22 @@ pub(crate) fn extract_name(cert: &X509Certificate, all: bool) -> Option<(String,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn extract_name_and_policies(
|
pub(crate) struct Metadata {
|
||||||
|
serial: Serial,
|
||||||
|
slot: RetiredSlotId,
|
||||||
|
name: String,
|
||||||
|
created: String,
|
||||||
|
pin_policy: Option<PinPolicy>,
|
||||||
|
touch_policy: Option<TouchPolicy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metadata {
|
||||||
|
pub(crate) fn extract(
|
||||||
yubikey: &mut YubiKey,
|
yubikey: &mut YubiKey,
|
||||||
key: &Key,
|
slot: RetiredSlotId,
|
||||||
cert: &X509Certificate,
|
cert: &X509Certificate,
|
||||||
all: bool,
|
all: bool,
|
||||||
) -> Option<(String, Option<PinPolicy>, Option<TouchPolicy>)> {
|
) -> Option<Self> {
|
||||||
// We store the PIN and touch policies for identities in their certificates
|
// We store the PIN and touch policies for identities in their certificates
|
||||||
// using the same certificate extension as PIV attestations.
|
// using the same certificate extension as PIV attestations.
|
||||||
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
|
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
|
||||||
@@ -123,14 +135,16 @@ pub(crate) fn extract_name_and_policies(
|
|||||||
.unwrap_or((None, None))
|
.unwrap_or((None, None))
|
||||||
};
|
};
|
||||||
|
|
||||||
extract_name(cert, all).map(|(name, ours)| {
|
extract_name(cert, all)
|
||||||
|
.map(|(name, ours)| {
|
||||||
if ours {
|
if ours {
|
||||||
let (pin_policy, touch_policy) = policies(&cert);
|
let (pin_policy, touch_policy) = policies(&cert);
|
||||||
(name, pin_policy, touch_policy)
|
(name, pin_policy, touch_policy)
|
||||||
} else {
|
} else {
|
||||||
// We can extract the PIN and touch policies via an attestation. This
|
// We can extract the PIN and touch policies via an attestation. This
|
||||||
// is slow, but the user has asked for all compatible keys, so...
|
// is slow, but the user has asked for all compatible keys, so...
|
||||||
let (pin_policy, touch_policy) = yubikey_piv::key::attest(yubikey, key.slot())
|
let (pin_policy, touch_policy) =
|
||||||
|
yubikey_piv::key::attest(yubikey, SlotId::Retired(slot))
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|buf| {
|
.and_then(|buf| {
|
||||||
x509_parser::parse_x509_certificate(&buf)
|
x509_parser::parse_x509_certificate(&buf)
|
||||||
@@ -142,15 +156,43 @@ pub(crate) fn extract_name_and_policies(
|
|||||||
(name, pin_policy, touch_policy)
|
(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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn print_identity(stub: Stub, recipient: Recipient, created: &str) {
|
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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user