Merge tag 'v0.4.0' into detect-critical-extensions

This commit is contained in:
Jack Grigg
2026-04-08 04:16:00 +01:00
20 changed files with 2112 additions and 644 deletions
+24 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+2
View File
@@ -249,6 +249,8 @@ impl IdentityPluginV1 for IdentityPlugin {
}
}
}
conn.disconnect_without_reset();
}
Ok(file_keys)
}
+9 -2
View File
@@ -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,
})