Implement --generate command

Includes logic to help users manage their keys:

- If the key is using a default PIN, we require the user to change it.
- We set the PUK equal to the PIN so the user doesn't need to remember
  them separately.
- We migrate the default management key to a new PIN-protected key.
This commit is contained in:
Jack Grigg
2021-01-03 19:47:10 +00:00
parent eedf9fa997
commit 850f96cd2c
7 changed files with 341 additions and 4 deletions
+153
View File
@@ -0,0 +1,153 @@
use rand::{rngs::OsRng, RngCore};
use x509::RelativeDistinguishedName;
use yubikey_piv::{
certificate::{Certificate, PublicKeyInfo},
key::{generate as yubikey_generate, AlgorithmId, RetiredSlotId, SlotId},
policy::{PinPolicy, TouchPolicy},
Key, YubiKey,
};
use crate::{
error::Error,
p256::Recipient,
util::POLICY_EXTENSION_OID,
yubikey::{self, Stub},
PLUGIN_NAME, USABLE_SLOTS,
};
const DEFAULT_PIN_POLICY: PinPolicy = PinPolicy::Once;
const DEFAULT_TOUCH_POLICY: TouchPolicy = TouchPolicy::Always;
pub(crate) struct IdentityBuilder {
slot: Option<RetiredSlotId>,
force: bool,
name: Option<String>,
pin_policy: Option<PinPolicy>,
touch_policy: Option<TouchPolicy>,
}
impl IdentityBuilder {
pub(crate) fn new(slot: Option<RetiredSlotId>) -> Self {
IdentityBuilder {
slot,
name: None,
pin_policy: None,
touch_policy: None,
force: false,
}
}
pub(crate) fn with_name(mut self, name: Option<String>) -> Self {
self.name = name;
self
}
pub(crate) fn with_pin_policy(mut self, pin_policy: Option<PinPolicy>) -> Self {
self.pin_policy = pin_policy;
self
}
pub(crate) fn with_touch_policy(mut self, touch_policy: Option<TouchPolicy>) -> Self {
self.touch_policy = touch_policy;
self
}
pub(crate) fn force(mut self, force: bool) -> Self {
self.force = force;
self
}
pub(crate) fn build(self, yubikey: &mut YubiKey) -> Result<(Stub, Recipient, String), Error> {
let slot = match self.slot {
Some(slot) => {
if !self.force {
// Check that the slot is empty.
if Key::list(yubikey)?
.into_iter()
.any(|key| key.slot() == SlotId::Retired(slot))
{
return Err(Error::SlotIsNotEmpty(slot));
}
}
// Now either the slot is empty, or --force is specified.
slot
}
None => {
// Use the first empty slot.
let keys = Key::list(yubikey)?;
USABLE_SLOTS
.iter()
.find(|&&slot| {
keys.iter()
.find(|key| key.slot() == SlotId::Retired(slot))
.is_none()
})
.cloned()
.ok_or(Error::NoEmptySlots(yubikey.serial()))?
}
};
let pin_policy = self.pin_policy.unwrap_or(DEFAULT_PIN_POLICY);
let touch_policy = self.touch_policy.unwrap_or(DEFAULT_TOUCH_POLICY);
// No need to ask for users to enter their PIN if the PIN policy requires it,
// because here we _always_ require them to enter their PIN in order to access the
// protected management key (which is necessary in order to generate identities).
yubikey::manage(yubikey)?;
if let TouchPolicy::Never = touch_policy {
// No need to touch YubiKey
} else {
eprintln!("👆 Please touch the YubiKey");
}
// Generate a new key in the selected slot.
let generated = yubikey_generate(
yubikey,
SlotId::Retired(slot),
AlgorithmId::EccP256,
pin_policy,
touch_policy,
)?;
let recipient = match &generated {
PublicKeyInfo::EcP256(pubkey) => {
Recipient::from_pubkey(*pubkey).expect("YubiKey generates a valid pubkey")
}
_ => unreachable!(),
};
let stub = Stub::new(yubikey.serial(), slot, &recipient);
// Pick a random serial for the new self-signed certificate.
let mut serial = [0; 20];
OsRng.fill_bytes(&mut serial);
let name = self
.name
.unwrap_or(format!("age identity {}", hex::encode(stub.tag)));
Certificate::generate_self_signed(
yubikey,
SlotId::Retired(slot),
serial,
None,
&[
RelativeDistinguishedName::organization(PLUGIN_NAME),
RelativeDistinguishedName::organizational_unit(env!("CARGO_PKG_VERSION")),
RelativeDistinguishedName::common_name(&name),
],
generated,
&[x509::Extension::regular(
POLICY_EXTENSION_OID,
&[pin_policy.into(), touch_policy.into()],
)],
)?;
Ok((
Stub::new(yubikey.serial(), slot, &recipient),
recipient,
chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
))
}
}
+33
View File
@@ -5,14 +5,20 @@ use yubikey_piv::{key::RetiredSlotId, Serial};
use crate::USABLE_SLOTS;
pub enum Error {
CustomManagementKey,
InvalidPinLength,
InvalidPinPolicy(String),
InvalidSlot(u8),
InvalidTouchPolicy(String),
Io(io::Error),
MultipleCommands,
MultipleIdentities,
MultipleYubiKeys,
NoEmptySlots(Serial),
NoIdentities,
NoMatchingSerial(Serial),
SlotHasNoIdentity(RetiredSlotId),
SlotIsNotEmpty(RetiredSlotId),
TimedOut,
YubiKey(yubikey_piv::Error),
}
@@ -34,11 +40,25 @@ impl From<yubikey_piv::error::Error> for Error {
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::CustomManagementKey => {
writeln!(f, "Custom unprotected management keys are not supported.")?
}
Error::InvalidPinLength => writeln!(f, "The PIN needs to be 1-8 characters.")?,
Error::InvalidPinPolicy(s) => writeln!(
f,
"Invalid PIN policy '{}' (expected [always, once, never]).",
s
)?,
Error::InvalidSlot(slot) => writeln!(
f,
"Invalid slot '{}' (expected number between 1 and 20).",
slot
)?,
Error::InvalidTouchPolicy(s) => writeln!(
f,
"Invalid touch policy '{}' (expected [always, cached, never]).",
s
)?,
Error::Io(e) => writeln!(f, "Failed to set up YubiKey: {}", e)?,
Error::MultipleCommands => writeln!(
f,
@@ -52,6 +72,9 @@ impl fmt::Debug for Error {
f,
"Multiple YubiKeys are plugged in. Use --serial to select a single YubiKey."
)?,
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.")?
}
@@ -63,6 +86,11 @@ impl fmt::Debug for Error {
"Slot {} does not contain an age identity or compatible key.",
USABLE_SLOTS.iter().position(|s| s == slot).unwrap() + 1
)?,
Error::SlotIsNotEmpty(slot) => writeln!(
f,
"Slot {} is not empty. Use --force to overwrite the slot.",
USABLE_SLOTS.iter().position(|s| s == slot).unwrap() + 1
)?,
Error::TimedOut => {
writeln!(f, "Timed out while waiting for a YubiKey to be inserted.")?
}
@@ -70,6 +98,11 @@ impl fmt::Debug for Error {
yubikey_piv::error::Error::NotFound => {
writeln!(f, "Please insert the YubiKey you want to set up")?
}
yubikey_piv::error::Error::WrongPin { tries } => writeln!(
f,
"Invalid PIN ({} tries remaining before it is blocked)",
tries
)?,
e => {
writeln!(f, "Error while communicating with YubiKey: {}", e)?;
use std::error::Error;
+54 -1
View File
@@ -7,6 +7,7 @@ use yubikey_piv::{
Key, Readers,
};
mod builder;
mod error;
mod p256;
mod plugin;
@@ -54,6 +55,9 @@ struct PluginOptions {
)]
age_plugin: Option<String>,
#[options(help = "Force --generate to overwrite a filled slot.")]
force: bool,
#[options(help = "Generate a new YubiKey identity.")]
generate: bool,
@@ -66,6 +70,15 @@ struct PluginOptions {
#[options(help = "List all YubiKey keys that are compatible with age.", no_short)]
list_all: bool,
#[options(
help = "Name for the generated identity. Defaults to 'age identity HEX_TAG'.",
no_short
)]
name: Option<String>,
#[options(help = "One of [always, once, never]. Defaults to 'once'.", no_short)]
pin_policy: Option<String>,
#[options(
help = "Specify which YubiKey to use, if more than one is plugged in.",
no_short
@@ -77,6 +90,46 @@ struct PluginOptions {
no_short
)]
slot: Option<u8>,
#[options(
help = "One of [always, cached, never]. Defaults to 'always'.",
no_short
)]
touch_policy: Option<String>,
}
fn generate(opts: PluginOptions) -> Result<(), Error> {
let serial = opts.serial.map(|s| s.into());
let slot = opts
.slot
.map(|slot| {
USABLE_SLOTS
.get(slot as usize - 1)
.cloned()
.ok_or(Error::InvalidSlot(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()?;
let mut yubikey = yubikey::open(serial)?;
let (stub, recipient, created) = builder::IdentityBuilder::new(slot)
.with_name(opts.name)
.with_pin_policy(pin_policy)
.with_touch_policy(touch_policy)
.force(opts.force)
.build(&mut yubikey)?;
util::print_identity(stub, recipient, &created);
Ok(())
}
fn identity(opts: PluginOptions) -> Result<(), Error> {
@@ -232,7 +285,7 @@ fn main() -> Result<(), Error> {
)?;
Ok(())
} else if opts.generate {
todo!()
generate(opts)
} else if opts.identity {
identity(opts)
} else if opts.list {
+20 -2
View File
@@ -4,9 +4,27 @@ use yubikey_piv::{
Key, YubiKey,
};
use crate::{p256::Recipient, yubikey::Stub, PLUGIN_NAME};
use crate::{error::Error, p256::Recipient, yubikey::Stub, PLUGIN_NAME};
const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8];
pub(crate) const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8];
pub(crate) fn pin_policy_from_string(s: String) -> Result<PinPolicy, Error> {
match s.as_str() {
"always" => Ok(PinPolicy::Always),
"once" => Ok(PinPolicy::Once),
"never" => Ok(PinPolicy::Never),
_ => Err(Error::InvalidPinPolicy(s)),
}
}
pub(crate) fn touch_policy_from_string(s: String) -> Result<TouchPolicy, Error> {
match s.as_str() {
"always" => Ok(TouchPolicy::Always),
"cached" => Ok(TouchPolicy::Cached),
"never" => Ok(TouchPolicy::Never),
_ => Err(Error::InvalidTouchPolicy(s)),
}
}
pub(crate) fn pin_policy_to_str(policy: Option<PinPolicy>) -> &'static str {
match policy {
+53 -1
View File
@@ -1,10 +1,11 @@
//! Structs for handling YubiKeys.
use bech32::{ToBase32, Variant};
use dialoguer::Password;
use std::fmt;
use std::thread::sleep;
use std::time::{Duration, SystemTime};
use yubikey_piv::{key::RetiredSlotId, yubikey::Serial, Readers, YubiKey};
use yubikey_piv::{key::RetiredSlotId, yubikey::Serial, MgmKey, Readers, YubiKey};
use crate::{
error::Error,
@@ -69,6 +70,57 @@ pub(crate) fn open(serial: Option<Serial>) -> Result<YubiKey, Error> {
Ok(yubikey)
}
pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
eprintln!("");
let pin = Password::new()
.with_prompt(&format!(
"Enter PIN for YubiKey with serial {} (default is 123456)",
yubikey.serial(),
))
.interact()?;
yubikey.verify_pin(pin.as_bytes())?;
// If the user is using the default PIN, help them to change it.
if pin == "123456" {
eprintln!("");
eprintln!("✨ Your key is using the default PIN. Let's change it!");
eprintln!("✨ We'll also set the PUK equal to the PIN.");
eprintln!("");
eprintln!("🔐 The PIN is up to 8 numbers, letters, or symbols. Not just numbers!");
eprintln!(
"❌ Your keys will be lost if the PIN and PUK are locked after 3 incorrect tries."
);
eprintln!("");
let current_puk = Password::new()
.with_prompt("Enter current PUK (default is 12345678)")
.interact()?;
let new_pin = Password::new()
.with_prompt("Choose a new PIN/PUK")
.with_confirmation("Repeat the PIN/PUK", "PINs don't match")
.interact()?;
if new_pin.len() > 8 {
return Err(Error::InvalidPinLength);
}
yubikey.change_puk(current_puk.as_bytes(), new_pin.as_bytes())?;
yubikey.change_pin(pin.as_bytes(), new_pin.as_bytes())?;
}
if let Ok(mgm_key) = MgmKey::get_protected(yubikey) {
yubikey.authenticate(mgm_key)?;
} else {
// Try to authenticate with the default management key.
yubikey
.authenticate(MgmKey::default())
.map_err(|_| Error::CustomManagementKey)?;
// Migrate to a PIN-protected management key.
let mgm_key = MgmKey::generate()?;
mgm_key.set_protected(yubikey)?;
}
Ok(())
}
/// A reference to an age key stored in a YubiKey.
#[derive(Debug)]
pub struct Stub {