Merge tag 'v0.4.0' into detect-critical-extensions
This commit is contained in:
+24
-6
@@ -1,3 +1,4 @@
|
||||
use dialoguer::Password;
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use x509::RelativeDistinguishedName;
|
||||
use yubikey::{
|
||||
@@ -8,6 +9,7 @@ use yubikey::{
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
fl,
|
||||
key::{self, Stub},
|
||||
p256::Recipient,
|
||||
util::{Metadata, POLICY_EXTENSION_OID},
|
||||
@@ -86,17 +88,13 @@ impl IdentityBuilder {
|
||||
let pin_policy = self.pin_policy.unwrap_or(DEFAULT_PIN_POLICY);
|
||||
let touch_policy = self.touch_policy.unwrap_or(DEFAULT_TOUCH_POLICY);
|
||||
|
||||
eprintln!("{}", fl!("builder-gen-key"));
|
||||
|
||||
// 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).
|
||||
key::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,
|
||||
@@ -109,6 +107,9 @@ impl IdentityBuilder {
|
||||
let recipient = Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey");
|
||||
let stub = Stub::new(yubikey.serial(), slot, &recipient);
|
||||
|
||||
eprintln!();
|
||||
eprintln!("{}", fl!("builder-gen-cert"));
|
||||
|
||||
// Pick a random serial for the new self-signed certificate.
|
||||
let mut serial = [0; 20];
|
||||
OsRng.fill_bytes(&mut serial);
|
||||
@@ -117,6 +118,23 @@ impl IdentityBuilder {
|
||||
.name
|
||||
.unwrap_or(format!("age identity {}", hex::encode(stub.tag)));
|
||||
|
||||
if let PinPolicy::Always = pin_policy {
|
||||
// We need to enter the PIN again.
|
||||
let pin = Password::new()
|
||||
.with_prompt(fl!(
|
||||
"plugin-enter-pin",
|
||||
yubikey_serial = yubikey.serial().to_string(),
|
||||
))
|
||||
.report(true)
|
||||
.interact()?;
|
||||
yubikey.verify_pin(pin.as_bytes())?;
|
||||
}
|
||||
if let TouchPolicy::Never = touch_policy {
|
||||
// No need to touch YubiKey
|
||||
} else {
|
||||
eprintln!("{}", fl!("builder-touch-yk"));
|
||||
}
|
||||
|
||||
let cert = Certificate::generate_self_signed(
|
||||
yubikey,
|
||||
SlotId::Retired(slot),
|
||||
|
||||
+56
-4
@@ -21,14 +21,17 @@ pub enum Error {
|
||||
InvalidSlot(u8),
|
||||
InvalidTouchPolicy(String),
|
||||
Io(io::Error),
|
||||
ManagementKeyAuth,
|
||||
MultipleCommands,
|
||||
MultipleYubiKeys,
|
||||
NoEmptySlots(Serial),
|
||||
NoMatchingSerial(Serial),
|
||||
PukLocked,
|
||||
SlotHasNoIdentity(RetiredSlotId),
|
||||
SlotIsNotEmpty(RetiredSlotId),
|
||||
TimedOut,
|
||||
UseListForSingleSlot,
|
||||
WrongPuk(u8),
|
||||
YubiKey(yubikey::Error),
|
||||
}
|
||||
|
||||
@@ -48,12 +51,19 @@ impl From<yubikey::Error> for Error {
|
||||
// manually to provide the error output we want.
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
const CHANGE_MGMT_KEY_CMD: &str =
|
||||
"ykman piv access change-management-key -a TDES --protect";
|
||||
const CHANGE_MGMT_KEY_URL: &str = "https://developers.yubico.com/yubikey-manager/";
|
||||
|
||||
match self {
|
||||
Error::CustomManagementKey => {
|
||||
wlnfl!(f, "err-custom-mgmt-key")?;
|
||||
let cmd = "ykman piv access change-management-key --protect";
|
||||
let url = "https://developers.yubico.com/yubikey-manager/";
|
||||
wlnfl!(f, "rec-custom-mgmt-key", cmd = cmd, url = url)?;
|
||||
wlnfl!(
|
||||
f,
|
||||
"rec-change-mgmt-key",
|
||||
cmd = CHANGE_MGMT_KEY_CMD,
|
||||
url = CHANGE_MGMT_KEY_URL
|
||||
)?;
|
||||
}
|
||||
Error::InvalidFlagCommand(flag, command) => wlnfl!(
|
||||
f,
|
||||
@@ -76,6 +86,17 @@ impl fmt::Debug for Error {
|
||||
expected = "always, cached, never",
|
||||
)?,
|
||||
Error::Io(e) => wlnfl!(f, "err-io", err = e.to_string())?,
|
||||
Error::ManagementKeyAuth => {
|
||||
let aes_url = "https://github.com/str4d/age-plugin-yubikey/issues/92";
|
||||
wlnfl!(f, "err-mgmt-key-auth")?;
|
||||
wlnfl!(f, "rec-mgmt-key-auth", aes_url = aes_url)?;
|
||||
wlnfl!(
|
||||
f,
|
||||
"rec-change-mgmt-key",
|
||||
cmd = CHANGE_MGMT_KEY_CMD,
|
||||
url = CHANGE_MGMT_KEY_URL
|
||||
)?;
|
||||
}
|
||||
Error::MultipleCommands => wlnfl!(f, "err-multiple-commands")?,
|
||||
Error::MultipleYubiKeys => wlnfl!(f, "err-multiple-yubikeys")?,
|
||||
Error::NoEmptySlots(serial) => {
|
||||
@@ -84,6 +105,7 @@ impl fmt::Debug for Error {
|
||||
Error::NoMatchingSerial(serial) => {
|
||||
wlnfl!(f, "err-no-matching-serial", serial = serial.to_string())?
|
||||
}
|
||||
Error::PukLocked => wlnfl!(f, "err-yk-pin-locked", pin_kind = "PUK")?,
|
||||
Error::SlotHasNoIdentity(slot) => {
|
||||
wlnfl!(f, "err-slot-has-no-identity", slot = slot_to_ui(slot))?
|
||||
}
|
||||
@@ -92,6 +114,9 @@ impl fmt::Debug for Error {
|
||||
}
|
||||
Error::TimedOut => wlnfl!(f, "err-timed-out")?,
|
||||
Error::UseListForSingleSlot => wlnfl!(f, "err-use-list-for-single")?,
|
||||
Error::WrongPuk(tries) => {
|
||||
wlnfl!(f, "err-yk-wrong-pin", pin_kind = "PUK", tries = tries)?
|
||||
}
|
||||
Error::YubiKey(e) => match e {
|
||||
yubikey::Error::NotFound => wlnfl!(f, "err-yk-not-found")?,
|
||||
yubikey::Error::PcscError {
|
||||
@@ -105,13 +130,40 @@ impl fmt::Debug for Error {
|
||||
wlnfl!(f, "err-yk-no-service-macos")?;
|
||||
let url = "https://apple.stackexchange.com/a/438198";
|
||||
wlnfl!(f, "rec-yk-no-service-macos", url = url)?;
|
||||
} else if cfg!(target_os = "openbsd") {
|
||||
wlnfl!(f, "err-yk-no-service-pcscd")?;
|
||||
let pkg = "pkg_add pcsc-lite ccid";
|
||||
let service_enable = "rcctl enable pcscd";
|
||||
let service_start = "rcctl start pcscd";
|
||||
wlnfl!(
|
||||
f,
|
||||
"rec-yk-no-service-pcscd-bsd",
|
||||
pkg = pkg,
|
||||
service_enable = service_enable,
|
||||
service_start = service_start
|
||||
)?;
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
wlnfl!(f, "err-yk-no-service-pcscd")?;
|
||||
let pkg = "pkg install pcsc-lite libccid";
|
||||
let service_enable = "service pcscd enable";
|
||||
let service_start = "service pcscd start";
|
||||
wlnfl!(
|
||||
f,
|
||||
"rec-yk-no-service-pcscd-bsd",
|
||||
pkg = pkg,
|
||||
service_enable = service_enable,
|
||||
service_start = service_start
|
||||
)?;
|
||||
} else {
|
||||
wlnfl!(f, "err-yk-no-service-pcscd")?;
|
||||
let apt = "sudo apt-get install pcscd";
|
||||
wlnfl!(f, "rec-yk-no-service-pcscd", apt = apt)?;
|
||||
}
|
||||
}
|
||||
yubikey::Error::WrongPin { tries } => wlnfl!(f, "err-yk-wrong-pin", tries = tries)?,
|
||||
yubikey::Error::PinLocked => wlnfl!(f, "err-yk-pin-locked", pin_kind = "PIN")?,
|
||||
yubikey::Error::WrongPin { tries } => {
|
||||
wlnfl!(f, "err-yk-wrong-pin", pin_kind = "PIN", tries = tries)?
|
||||
}
|
||||
e => {
|
||||
wlnfl!(f, "err-yk-general", err = e.to_string())?;
|
||||
use std::error::Error;
|
||||
|
||||
+30
-13
@@ -1,10 +1,15 @@
|
||||
use age_core::{
|
||||
format::{FileKey, Stanza},
|
||||
primitives::{aead_encrypt, hkdf},
|
||||
primitives::aead_encrypt,
|
||||
secrecy::ExposeSecret,
|
||||
};
|
||||
use p256::{ecdh::EphemeralSecret, elliptic_curve::sec1::ToEncodedPoint};
|
||||
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
|
||||
use p256::{
|
||||
ecdh::EphemeralSecret,
|
||||
elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint},
|
||||
};
|
||||
use rand::rngs::OsRng;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::{p256::Recipient, STANZA_TAG};
|
||||
|
||||
@@ -22,8 +27,12 @@ pub(crate) struct EphemeralKeyBytes(p256::EncodedPoint);
|
||||
|
||||
impl EphemeralKeyBytes {
|
||||
fn from_bytes(bytes: [u8; EPK_BYTES]) -> Option<Self> {
|
||||
let encoded = p256::EncodedPoint::from_bytes(&bytes).ok()?;
|
||||
if encoded.is_compressed() && encoded.decompress().is_some() {
|
||||
let encoded = p256::EncodedPoint::from_bytes(bytes).ok()?;
|
||||
if encoded.is_compressed()
|
||||
&& p256::PublicKey::from_encoded_point(&encoded)
|
||||
.is_some()
|
||||
.into()
|
||||
{
|
||||
Some(EphemeralKeyBytes(encoded))
|
||||
} else {
|
||||
None
|
||||
@@ -39,9 +48,9 @@ impl EphemeralKeyBytes {
|
||||
}
|
||||
|
||||
pub(crate) fn decompress(&self) -> p256::EncodedPoint {
|
||||
self.0
|
||||
.decompress()
|
||||
.expect("EphemeralKeyBytes is a valid compressed encoding by construction")
|
||||
// EphemeralKeyBytes is a valid compressed encoding by construction.
|
||||
let p = p256::PublicKey::from_encoded_point(&self.0).unwrap();
|
||||
p.to_encoded_point(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +66,8 @@ impl From<RecipientLine> for Stanza {
|
||||
Stanza {
|
||||
tag: STANZA_TAG.to_owned(),
|
||||
args: vec![
|
||||
base64::encode_config(&r.tag, base64::STANDARD_NO_PAD),
|
||||
base64::encode_config(r.epk_bytes.as_bytes(), base64::STANDARD_NO_PAD),
|
||||
BASE64_STANDARD_NO_PAD.encode(r.tag),
|
||||
BASE64_STANDARD_NO_PAD.encode(r.epk_bytes.as_bytes()),
|
||||
],
|
||||
body: r.encrypted_file_key.to_vec(),
|
||||
}
|
||||
@@ -76,9 +85,10 @@ impl RecipientLine {
|
||||
return None;
|
||||
}
|
||||
|
||||
base64::decode_config_slice(arg, base64::STANDARD_NO_PAD, buf.as_mut())
|
||||
BASE64_STANDARD_NO_PAD
|
||||
.decode_slice_unchecked(arg, buf.as_mut())
|
||||
.ok()
|
||||
.map(|_| buf)
|
||||
.and_then(|len| (len == buf.as_mut().len()).then_some(buf))
|
||||
}
|
||||
|
||||
let (tag, epk_bytes) = match &s.args[..] {
|
||||
@@ -101,7 +111,7 @@ impl RecipientLine {
|
||||
}
|
||||
|
||||
pub(crate) fn wrap_file_key(file_key: &FileKey, pk: &Recipient) -> Self {
|
||||
let esk = EphemeralSecret::random(OsRng);
|
||||
let esk = EphemeralSecret::random(&mut OsRng);
|
||||
let epk = esk.public_key();
|
||||
let epk_bytes = EphemeralKeyBytes::from_public_key(&epk);
|
||||
|
||||
@@ -111,7 +121,14 @@ impl RecipientLine {
|
||||
salt.extend_from_slice(epk_bytes.as_bytes());
|
||||
salt.extend_from_slice(pk.to_encoded().as_bytes());
|
||||
|
||||
let enc_key = hkdf(&salt, STANZA_KEY_LABEL, shared_secret.as_bytes());
|
||||
let enc_key = {
|
||||
let mut okm = [0; 32];
|
||||
shared_secret
|
||||
.extract::<Sha256>(Some(&salt))
|
||||
.expand(STANZA_KEY_LABEL, &mut okm)
|
||||
.expect("okm is the correct length");
|
||||
okm
|
||||
};
|
||||
|
||||
let encrypted_file_key = {
|
||||
let mut key = [0; ENCRYPTED_FILE_KEY_BYTES];
|
||||
|
||||
+85
-29
@@ -53,7 +53,24 @@ pub(crate) fn filter_connected(reader: &Reader) -> bool {
|
||||
);
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
Err(yubikey::Error::AppletNotFound { applet_name }) => {
|
||||
warn!(
|
||||
"{}",
|
||||
fl!(
|
||||
"warn-yk-missing-applet",
|
||||
yubikey_name = reader.name(),
|
||||
applet_name = applet_name,
|
||||
),
|
||||
);
|
||||
false
|
||||
}
|
||||
Err(_) => true,
|
||||
Ok(yubikey) => {
|
||||
// We only connected as a side-effect of confirming that we can connect, so
|
||||
// avoid resetting the YubiKey.
|
||||
disconnect_without_reset(yubikey);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +202,9 @@ fn open_by_serial(serial: Serial) -> Result<YubiKey, yubikey::Error> {
|
||||
|
||||
if serial == yubikey.serial() {
|
||||
return Ok(yubikey);
|
||||
} else {
|
||||
// We didn't want this YubiKey; don't reset it.
|
||||
disconnect_without_reset(yubikey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +261,23 @@ pub(crate) fn open(serial: Option<Serial>) -> Result<YubiKey, Error> {
|
||||
Ok(yubikey)
|
||||
}
|
||||
|
||||
/// Disconnect from the YubiKey without resetting it.
|
||||
///
|
||||
/// This can be used to preserve the YubiKey's PIN and touch caches. There are two cases
|
||||
/// where we want to do this:
|
||||
///
|
||||
/// - We connected to this YubiKey in a read-only context, so we have not made any changes
|
||||
/// to the YubiKey's state. However, we might have asked an agent to release the YubiKey
|
||||
/// in `key::open_connection`, and we want to allow any state it may have left behind
|
||||
/// (such as cached PINs or touches) to persist beyond our execution, for usability.
|
||||
/// - We opened this connection in a decryption context, so the only changes to the
|
||||
/// YubiKey's state were to potentially cache the PIN and/or touch (depending on the
|
||||
/// policies of the slot). We want to allow these to persist beyond our execution, for
|
||||
/// usability.
|
||||
pub(crate) fn disconnect_without_reset(yubikey: YubiKey) {
|
||||
let _ = yubikey.disconnect(pcsc::Disposition::LeaveCard);
|
||||
}
|
||||
|
||||
fn request_pin<E>(
|
||||
mut prompt: impl FnMut(Option<String>) -> io::Result<Result<SecretString, E>>,
|
||||
serial: Serial,
|
||||
@@ -277,6 +314,7 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
|
||||
yubikey_serial = yubikey.serial().to_string(),
|
||||
default_pin = DEFAULT_PIN,
|
||||
))
|
||||
.report(true)
|
||||
.interact()?;
|
||||
yubikey.verify_pin(pin.as_bytes())?;
|
||||
|
||||
@@ -310,34 +348,45 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
|
||||
}
|
||||
};
|
||||
let new_pin = new_pin.expose_secret();
|
||||
yubikey.change_puk(current_puk.as_bytes(), new_pin.as_bytes())?;
|
||||
yubikey
|
||||
.change_puk(current_puk.as_bytes(), new_pin.as_bytes())
|
||||
.map_err(|e| match e {
|
||||
yubikey::Error::PinLocked => Error::PukLocked,
|
||||
yubikey::Error::WrongPin { tries } => Error::WrongPuk(tries),
|
||||
_ => Error::YubiKey(e),
|
||||
})?;
|
||||
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)?;
|
||||
match MgmKey::get_protected(yubikey) {
|
||||
Ok(mgm_key) => yubikey.authenticate(mgm_key).map_err(|e| match e {
|
||||
yubikey::Error::AuthenticationError => Error::ManagementKeyAuth,
|
||||
_ => e.into(),
|
||||
})?,
|
||||
Err(yubikey::Error::AuthenticationError) => Err(Error::ManagementKeyAuth)?,
|
||||
_ => {
|
||||
// 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();
|
||||
eprintln!();
|
||||
eprintln!("{}", fl!("mgr-changing-mgmt-key"));
|
||||
eprint!("... ");
|
||||
mgm_key.set_protected(yubikey).map_err(|e| {
|
||||
eprintln!(
|
||||
"{}",
|
||||
fl!(
|
||||
"mgr-changing-mgmt-key-error",
|
||||
management_key = hex::encode(mgm_key.as_ref()),
|
||||
)
|
||||
);
|
||||
e
|
||||
})?;
|
||||
eprintln!("{}", fl!("mgr-changing-mgmt-key-success"));
|
||||
// Migrate to a PIN-protected management key.
|
||||
let mgm_key = MgmKey::generate();
|
||||
eprintln!();
|
||||
eprintln!("{}", fl!("mgr-changing-mgmt-key"));
|
||||
eprint!("... ");
|
||||
mgm_key.set_protected(yubikey).map_err(|e| {
|
||||
eprintln!(
|
||||
"{}",
|
||||
fl!(
|
||||
"mgr-changing-mgmt-key-error",
|
||||
management_key = hex::encode(mgm_key.as_ref()),
|
||||
)
|
||||
);
|
||||
e
|
||||
})?;
|
||||
eprintln!("{}", fl!("mgr-changing-mgmt-key-success"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -642,13 +691,13 @@ impl Connection {
|
||||
metadata => metadata,
|
||||
};
|
||||
}
|
||||
if let Some(PinPolicy::Never) = self.cached_metadata.as_ref().and_then(|m| m.pin_policy) {
|
||||
return Ok(Ok(()));
|
||||
match self.cached_metadata.as_ref().and_then(|m| m.pin_policy) {
|
||||
Some(PinPolicy::Never) => return Ok(Ok(())),
|
||||
Some(PinPolicy::Once) if self.yubikey.verify_pin(&[]).is_ok() => return Ok(Ok(())),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// The policy requires a PIN, so request it.
|
||||
// Note that we can't distinguish between PinPolicy::Once and PinPolicy::Always
|
||||
// because this plugin is ephemeral, so we always request the PIN.
|
||||
let pin = match request_pin(
|
||||
|prev_error| {
|
||||
callbacks.request_secret(&format!(
|
||||
@@ -732,6 +781,13 @@ impl Connection {
|
||||
Err(_) => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Close this connection without resetting the YubiKey.
|
||||
///
|
||||
/// This can be used to preserve the YubiKey's PIN and touch caches.
|
||||
pub(crate) fn disconnect_without_reset(self) {
|
||||
disconnect_without_reset(self.yubikey);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+80
-26
@@ -181,6 +181,13 @@ fn generate(flags: PluginFlags) -> Result<(), Error> {
|
||||
|
||||
util::print_identity(stub, recipient, metadata);
|
||||
|
||||
// We have written to the YubiKey, which means we've authenticated with the management
|
||||
// key. Out of an abundance of caution, we let the YubiKey be reset on disconnect,
|
||||
// which will clear its PIN and touch caches. This has as small negative UX effect,
|
||||
// but identity generation is a relatively infrequent occurrence, and users are more
|
||||
// likely to see their cached PINs reset due to switching applets (e.g. from PIV to
|
||||
// FIDO2).
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -200,6 +207,8 @@ fn print_single(
|
||||
|
||||
printer(stub, recipient, metadata);
|
||||
|
||||
key::disconnect_without_reset(yubikey);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -233,6 +242,8 @@ fn print_multiple(
|
||||
println!();
|
||||
}
|
||||
println!();
|
||||
|
||||
key::disconnect_without_reset(yubikey);
|
||||
}
|
||||
if printed > 1 {
|
||||
eprintln!("{}", fl!("printed-multiple", kind = kind, count = printed));
|
||||
@@ -286,7 +297,7 @@ fn list(flags: PluginFlags, all: bool) -> Result<(), Error> {
|
||||
all,
|
||||
|_, recipient, metadata| {
|
||||
println!("{}", metadata);
|
||||
println!("{}", recipient.to_string());
|
||||
println!("{}", recipient);
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -360,11 +371,13 @@ fn main() -> Result<(), Error> {
|
||||
.iter()
|
||||
.map(|reader| {
|
||||
key::open_connection(reader).map(|yk| {
|
||||
fl!(
|
||||
let name = fl!(
|
||||
"cli-setup-yk-name",
|
||||
yubikey_name = reader.name(),
|
||||
yubikey_serial = yk.serial().to_string(),
|
||||
)
|
||||
);
|
||||
key::disconnect_without_reset(yk);
|
||||
name
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
@@ -372,6 +385,7 @@ fn main() -> Result<(), Error> {
|
||||
.with_prompt(fl!("cli-setup-select-yk"))
|
||||
.items(&reader_names)
|
||||
.default(0)
|
||||
.report(true)
|
||||
.interact_opt()?
|
||||
{
|
||||
Some(yk) => readers_list[yk].open()?,
|
||||
@@ -393,7 +407,11 @@ fn main() -> Result<(), Error> {
|
||||
x509_parser::parse_x509_certificate(key.certificate().as_ref())
|
||||
.unwrap();
|
||||
let (name, _) = util::extract_name(&cert, true).unwrap();
|
||||
let created = cert.validity().not_before.to_rfc2822();
|
||||
let created = cert
|
||||
.validity()
|
||||
.not_before
|
||||
.to_rfc2822()
|
||||
.unwrap_or_else(|e| format!("Invalid date: {}", e));
|
||||
|
||||
format!("{}, created: {}", name, created)
|
||||
})
|
||||
@@ -426,6 +444,7 @@ fn main() -> Result<(), Error> {
|
||||
.with_prompt(fl!("cli-setup-select-slot"))
|
||||
.items(&slots)
|
||||
.default(0)
|
||||
.report(true)
|
||||
.interact_opt()?
|
||||
{
|
||||
Some(slot) => {
|
||||
@@ -443,6 +462,7 @@ fn main() -> Result<(), Error> {
|
||||
|
||||
if Confirm::new()
|
||||
.with_prompt(fl!("cli-setup-use-existing", slot_index = slot_index))
|
||||
.report(true)
|
||||
.interact()?
|
||||
{
|
||||
let stub = key::Stub::new(yubikey.serial(), slot, &recipient);
|
||||
@@ -450,8 +470,10 @@ fn main() -> Result<(), Error> {
|
||||
util::Metadata::extract(&mut yubikey, slot, key.certificate(), true)
|
||||
.unwrap();
|
||||
|
||||
key::disconnect_without_reset(yubikey);
|
||||
((stub, recipient, metadata), false)
|
||||
} else {
|
||||
key::disconnect_without_reset(yubikey);
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
@@ -462,30 +484,57 @@ fn main() -> Result<(), Error> {
|
||||
flags.name.as_deref().unwrap_or("age identity TAG_HEX")
|
||||
))
|
||||
.allow_empty(true)
|
||||
.report(true)
|
||||
.interact_text()?;
|
||||
|
||||
let pin_policy = match Select::new()
|
||||
.with_prompt(fl!("cli-setup-select-pin-policy"))
|
||||
.items(&[
|
||||
fl!("pin-policy-always"),
|
||||
fl!("pin-policy-once"),
|
||||
fl!("pin-policy-never"),
|
||||
])
|
||||
.default(
|
||||
[PinPolicy::Always, PinPolicy::Once, PinPolicy::Never]
|
||||
.iter()
|
||||
.position(|p| {
|
||||
p == &flags.pin_policy.unwrap_or(builder::DEFAULT_PIN_POLICY)
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.interact_opt()?
|
||||
{
|
||||
Some(0) => PinPolicy::Always,
|
||||
Some(1) => PinPolicy::Once,
|
||||
Some(2) => PinPolicy::Never,
|
||||
Some(_) => unreachable!(),
|
||||
None => return Ok(()),
|
||||
let mut displayed_yk4_warning = false;
|
||||
let pin_policy = loop {
|
||||
let pin_policy = match Select::new()
|
||||
.with_prompt(fl!("cli-setup-select-pin-policy"))
|
||||
.items(&[
|
||||
fl!("pin-policy-always"),
|
||||
fl!("pin-policy-once"),
|
||||
fl!("pin-policy-never"),
|
||||
])
|
||||
.default(
|
||||
[PinPolicy::Always, PinPolicy::Once, PinPolicy::Never]
|
||||
.iter()
|
||||
.position(|p| {
|
||||
p == &flags.pin_policy.unwrap_or(builder::DEFAULT_PIN_POLICY)
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.report(true)
|
||||
.interact_opt()?
|
||||
{
|
||||
Some(0) => PinPolicy::Always,
|
||||
Some(1) => PinPolicy::Once,
|
||||
Some(2) => PinPolicy::Never,
|
||||
Some(_) => unreachable!(),
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// We can't preserve the PIN cache for YubiKey 4 series, because to
|
||||
// retrieve the serial we switch to the OTP applet.
|
||||
match (pin_policy, yubikey.version().major) {
|
||||
(PinPolicy::Once, 4) => {
|
||||
if !displayed_yk4_warning {
|
||||
eprintln!();
|
||||
eprintln!("{}", fl!("cli-setup-yk4-pin-policy"));
|
||||
eprintln!();
|
||||
displayed_yk4_warning = true;
|
||||
}
|
||||
|
||||
if Confirm::new()
|
||||
.with_prompt(fl!("cli-setup-yk4-pin-policy-confirm"))
|
||||
.report(true)
|
||||
.interact()?
|
||||
{
|
||||
break pin_policy;
|
||||
}
|
||||
}
|
||||
_ => break pin_policy,
|
||||
}
|
||||
};
|
||||
|
||||
let touch_policy = match Select::new()
|
||||
@@ -503,6 +552,7 @@ fn main() -> Result<(), Error> {
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.report(true)
|
||||
.interact_opt()?
|
||||
{
|
||||
Some(0) => TouchPolicy::Always,
|
||||
@@ -514,6 +564,7 @@ fn main() -> Result<(), Error> {
|
||||
|
||||
if Confirm::new()
|
||||
.with_prompt(fl!("cli-setup-generate-new", slot_index = slot_index))
|
||||
.report(true)
|
||||
.interact()?
|
||||
{
|
||||
eprintln!();
|
||||
@@ -529,6 +580,7 @@ fn main() -> Result<(), Error> {
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
key::disconnect_without_reset(yubikey);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -541,6 +593,7 @@ fn main() -> Result<(), Error> {
|
||||
"age-yubikey-identity-{}.txt",
|
||||
hex::encode(stub.tag)
|
||||
))
|
||||
.report(true)
|
||||
.interact_text()?;
|
||||
|
||||
let mut file = match OpenOptions::new()
|
||||
@@ -552,6 +605,7 @@ fn main() -> Result<(), Error> {
|
||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
|
||||
if Confirm::new()
|
||||
.with_prompt(fl!("cli-setup-identity-file-exists"))
|
||||
.report(true)
|
||||
.interact()?
|
||||
{
|
||||
File::create(&file_name)?
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ impl Recipient {
|
||||
/// This accepts both compressed (as used by the plugin) and uncompressed (as used in
|
||||
/// the YubiKey certificate) encodings.
|
||||
fn from_encoded(encoded: &p256::EncodedPoint) -> Option<Self> {
|
||||
p256::PublicKey::from_encoded_point(encoded).map(Recipient)
|
||||
Option::from(p256::PublicKey::from_encoded_point(encoded)).map(Recipient)
|
||||
}
|
||||
|
||||
/// Returns the compressed SEC-1 encoding of this recipient.
|
||||
|
||||
@@ -249,6 +249,8 @@ impl IdentityPluginV1 for IdentityPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conn.disconnect_without_reset();
|
||||
}
|
||||
Ok(file_keys)
|
||||
}
|
||||
|
||||
+9
-2
@@ -122,7 +122,10 @@ impl Metadata {
|
||||
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
|
||||
let policies = |c: &X509Certificate| {
|
||||
c.tbs_certificate
|
||||
.find_extension(&Oid::from(POLICY_EXTENSION_OID).unwrap())
|
||||
.get_extension_unique(&Oid::from(POLICY_EXTENSION_OID).unwrap())
|
||||
// If the extension is duplicated, we assume it is invalid.
|
||||
.ok()
|
||||
.flatten()
|
||||
// If the encoded extension doesn't have 2 bytes, we assume it is invalid.
|
||||
.filter(|policy| policy.value.len() >= 2)
|
||||
.map(|policy| {
|
||||
@@ -170,7 +173,11 @@ impl Metadata {
|
||||
serial: yubikey.serial(),
|
||||
slot,
|
||||
name,
|
||||
created: cert.validity().not_before.to_rfc2822(),
|
||||
created: cert
|
||||
.validity()
|
||||
.not_before
|
||||
.to_rfc2822()
|
||||
.unwrap_or_else(|e| format!("Invalid date: {}", e)),
|
||||
pin_policy,
|
||||
touch_policy,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user