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:
Generated
+23
@@ -44,13 +44,18 @@ dependencies = [
|
|||||||
"age-core",
|
"age-core",
|
||||||
"age-plugin",
|
"age-plugin",
|
||||||
"bech32",
|
"bech32",
|
||||||
|
"chrono",
|
||||||
"console",
|
"console",
|
||||||
|
"dialoguer",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"gumdrop",
|
"gumdrop",
|
||||||
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
"p256",
|
"p256",
|
||||||
|
"rand 0.8.3",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"x509",
|
||||||
"x509-parser",
|
"x509-parser",
|
||||||
"yubikey-piv",
|
"yubikey-piv",
|
||||||
]
|
]
|
||||||
@@ -299,6 +304,18 @@ dependencies = [
|
|||||||
"opaque-debug",
|
"opaque-debug",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dialoguer"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9dd058f8b65922819fabb4a41e7d1964e56344042c26efbccd465202c23fa0c"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"lazy_static",
|
||||||
|
"tempfile",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -443,6 +460,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hkdf"
|
name = "hkdf"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
|||||||
@@ -14,13 +14,18 @@ edition = "2018"
|
|||||||
age-core = "0.5"
|
age-core = "0.5"
|
||||||
age-plugin = "0.0"
|
age-plugin = "0.0"
|
||||||
bech32 = "0.8"
|
bech32 = "0.8"
|
||||||
|
chrono = "0.4"
|
||||||
console = "0.14"
|
console = "0.14"
|
||||||
|
dialoguer = "0.8"
|
||||||
elliptic-curve = "0.8"
|
elliptic-curve = "0.8"
|
||||||
env_logger = "0.8"
|
env_logger = "0.8"
|
||||||
gumdrop = "0.8"
|
gumdrop = "0.8"
|
||||||
|
hex = "0.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
p256 = "0.7"
|
p256 = "0.7"
|
||||||
|
rand = "0.8"
|
||||||
sha2 = "0.9"
|
sha2 = "0.9"
|
||||||
|
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"] }
|
||||||
|
|
||||||
|
|||||||
+153
@@ -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),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,14 +5,20 @@ use yubikey_piv::{key::RetiredSlotId, Serial};
|
|||||||
use crate::USABLE_SLOTS;
|
use crate::USABLE_SLOTS;
|
||||||
|
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
CustomManagementKey,
|
||||||
|
InvalidPinLength,
|
||||||
|
InvalidPinPolicy(String),
|
||||||
InvalidSlot(u8),
|
InvalidSlot(u8),
|
||||||
|
InvalidTouchPolicy(String),
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
MultipleCommands,
|
MultipleCommands,
|
||||||
MultipleIdentities,
|
MultipleIdentities,
|
||||||
MultipleYubiKeys,
|
MultipleYubiKeys,
|
||||||
|
NoEmptySlots(Serial),
|
||||||
NoIdentities,
|
NoIdentities,
|
||||||
NoMatchingSerial(Serial),
|
NoMatchingSerial(Serial),
|
||||||
SlotHasNoIdentity(RetiredSlotId),
|
SlotHasNoIdentity(RetiredSlotId),
|
||||||
|
SlotIsNotEmpty(RetiredSlotId),
|
||||||
TimedOut,
|
TimedOut,
|
||||||
YubiKey(yubikey_piv::Error),
|
YubiKey(yubikey_piv::Error),
|
||||||
}
|
}
|
||||||
@@ -34,11 +40,25 @@ impl From<yubikey_piv::error::Error> for Error {
|
|||||||
impl fmt::Debug for Error {
|
impl fmt::Debug for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
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!(
|
Error::InvalidSlot(slot) => writeln!(
|
||||||
f,
|
f,
|
||||||
"Invalid slot '{}' (expected number between 1 and 20).",
|
"Invalid slot '{}' (expected number between 1 and 20).",
|
||||||
slot
|
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::Io(e) => writeln!(f, "Failed to set up YubiKey: {}", e)?,
|
||||||
Error::MultipleCommands => writeln!(
|
Error::MultipleCommands => writeln!(
|
||||||
f,
|
f,
|
||||||
@@ -52,6 +72,9 @@ impl fmt::Debug for Error {
|
|||||||
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."
|
||||||
)?,
|
)?,
|
||||||
|
Error::NoEmptySlots(serial) => {
|
||||||
|
writeln!(f, "YubiKey with serial {} has no empty slots.", serial)?
|
||||||
|
}
|
||||||
Error::NoIdentities => {
|
Error::NoIdentities => {
|
||||||
writeln!(f, "This YubiKey does not contain any age identities.")?
|
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.",
|
"Slot {} does not contain an age identity or compatible key.",
|
||||||
USABLE_SLOTS.iter().position(|s| s == slot).unwrap() + 1
|
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 => {
|
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.")?
|
||||||
}
|
}
|
||||||
@@ -70,6 +98,11 @@ impl fmt::Debug for Error {
|
|||||||
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")?
|
||||||
}
|
}
|
||||||
|
yubikey_piv::error::Error::WrongPin { tries } => writeln!(
|
||||||
|
f,
|
||||||
|
"Invalid PIN ({} tries remaining before it is blocked)",
|
||||||
|
tries
|
||||||
|
)?,
|
||||||
e => {
|
e => {
|
||||||
writeln!(f, "Error while communicating with YubiKey: {}", e)?;
|
writeln!(f, "Error while communicating with YubiKey: {}", e)?;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|||||||
+54
-1
@@ -7,6 +7,7 @@ use yubikey_piv::{
|
|||||||
Key, Readers,
|
Key, Readers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod builder;
|
||||||
mod error;
|
mod error;
|
||||||
mod p256;
|
mod p256;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
@@ -54,6 +55,9 @@ struct PluginOptions {
|
|||||||
)]
|
)]
|
||||||
age_plugin: Option<String>,
|
age_plugin: Option<String>,
|
||||||
|
|
||||||
|
#[options(help = "Force --generate to overwrite a filled slot.")]
|
||||||
|
force: bool,
|
||||||
|
|
||||||
#[options(help = "Generate a new YubiKey identity.")]
|
#[options(help = "Generate a new YubiKey identity.")]
|
||||||
generate: bool,
|
generate: bool,
|
||||||
|
|
||||||
@@ -66,6 +70,15 @@ struct PluginOptions {
|
|||||||
#[options(help = "List all YubiKey keys that are compatible with age.", no_short)]
|
#[options(help = "List all YubiKey keys that are compatible with age.", no_short)]
|
||||||
list_all: bool,
|
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(
|
#[options(
|
||||||
help = "Specify which YubiKey to use, if more than one is plugged in.",
|
help = "Specify which YubiKey to use, if more than one is plugged in.",
|
||||||
no_short
|
no_short
|
||||||
@@ -77,6 +90,46 @@ struct PluginOptions {
|
|||||||
no_short
|
no_short
|
||||||
)]
|
)]
|
||||||
slot: Option<u8>,
|
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> {
|
fn identity(opts: PluginOptions) -> Result<(), Error> {
|
||||||
@@ -232,7 +285,7 @@ fn main() -> Result<(), Error> {
|
|||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else if opts.generate {
|
} else if opts.generate {
|
||||||
todo!()
|
generate(opts)
|
||||||
} else if opts.identity {
|
} else if opts.identity {
|
||||||
identity(opts)
|
identity(opts)
|
||||||
} else if opts.list {
|
} else if opts.list {
|
||||||
|
|||||||
+20
-2
@@ -4,9 +4,27 @@ use yubikey_piv::{
|
|||||||
Key, YubiKey,
|
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 {
|
pub(crate) fn pin_policy_to_str(policy: Option<PinPolicy>) -> &'static str {
|
||||||
match policy {
|
match policy {
|
||||||
|
|||||||
+53
-1
@@ -1,10 +1,11 @@
|
|||||||
//! Structs for handling YubiKeys.
|
//! Structs for handling YubiKeys.
|
||||||
|
|
||||||
use bech32::{ToBase32, Variant};
|
use bech32::{ToBase32, Variant};
|
||||||
|
use dialoguer::Password;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::{Duration, SystemTime};
|
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::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
@@ -69,6 +70,57 @@ pub(crate) fn open(serial: Option<Serial>) -> Result<YubiKey, Error> {
|
|||||||
Ok(yubikey)
|
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.
|
/// A reference to an age key stored in a YubiKey.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Stub {
|
pub struct Stub {
|
||||||
|
|||||||
Reference in New Issue
Block a user