#[cfg(feature = "virtual-piv")]
use crate::piv::VirtualPiv;
use crate::{
auxiliaries::{self, ProtectionMode},
errors::{CardError, PcscCode, PcscOp, YbError},
piv::{hardware::HardwarePiv, DeviceInfo, PivBackend},
policy::{self, SecretOp},
};
use anyhow::{bail, Context as _, Result};
use p256::PublicKey;
use std::cell::RefCell;
use std::sync::Arc;
use zeroize::Zeroizing;
#[derive(Debug, Clone, Copy, Default)]
pub struct OutputOptions {
pub debug: bool,
pub quiet: bool,
}
pub type DevicePicker = Box<
dyn Fn(
&Arc<dyn PivBackend>,
&[DeviceInfo],
) -> Result<Option<(DeviceInfo, Option<Box<dyn crate::piv::FlashHandle>>)>>,
>;
#[derive(Debug, Default)]
pub struct ContextOptions {
pub serial: Option<u32>,
pub reader: Option<String>,
pub management_key: Option<String>,
pub pin: Option<String>,
pub allow_defaults: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeySource {
Explicit,
Printed,
PrintedPrevious,
FactoryDefault,
}
impl KeySource {
pub fn is_printed(self) -> bool {
matches!(self, Self::Printed | Self::PrintedPrevious)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum PrintedRepair {
#[default]
None,
Rewrite,
Delete,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Repairs {
pub flags: bool,
pub printed: PrintedRepair,
}
impl Repairs {
pub fn any(&self) -> bool {
self.flags || self.printed != PrintedRepair::None
}
}
pub const MANAGEMENT_KEY_NOT_FOUND: &str =
"the management key is not in PRINTED and is not the factory default";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlotKeyCheck {
NoCertificate,
Match,
Mismatch,
}
struct ResolvedKey {
key: Zeroizing<String>,
source: KeySource,
repairs: Repairs,
}
impl ResolvedKey {
fn new(key: &str, source: KeySource, repairs: Repairs) -> Self {
Self {
key: Zeroizing::new(key.to_owned()),
source,
repairs,
}
}
}
pub struct Context {
pub reader: String,
pub serial: u32,
pub firmware: String,
pub management_key: Option<String>,
pub defaults: auxiliaries::DefaultCredentials,
pub allow_defaults: bool,
pin: RefCell<Option<Zeroizing<String>>>,
pin_fn: Box<dyn Fn() -> Result<Option<String>>>,
pub piv: Arc<dyn PivBackend>,
pub debug: bool,
pub quiet: bool,
pub protection: ProtectionMode,
resolved: RefCell<Option<ResolvedKey>>,
pub flash_handle: Option<Box<dyn crate::piv::FlashHandle>>,
}
impl Context {
pub fn new(
opts: ContextOptions,
pin_fn: Box<dyn Fn() -> Result<Option<String>>>,
device_picker: DevicePicker,
output: OutputOptions,
) -> Result<Self> {
let debug = output.debug;
let quiet = output.quiet;
#[cfg(feature = "virtual-piv")]
let piv: Arc<dyn PivBackend> = if let Ok(path) = std::env::var("YB_FIXTURE") {
Arc::new(VirtualPiv::from_fixture(std::path::Path::new(&path))?)
} else {
Arc::new(HardwarePiv::new())
};
#[cfg(not(feature = "virtual-piv"))]
let piv: Arc<dyn PivBackend> = Arc::new(HardwarePiv::new());
let devices = piv.list_devices().context("listing YubiKey devices")?;
let (device, selected_reader, flash_handle) = select_device(
&devices,
opts.serial.as_ref(),
opts.reader.as_deref(),
&piv,
&*device_picker,
)?;
let (defaults, protection) = probe_card(&selected_reader, piv.as_ref());
Ok(Self {
reader: selected_reader,
serial: device.serial,
firmware: device.version.clone(),
management_key: opts.management_key,
defaults,
allow_defaults: opts.allow_defaults,
pin: initial_pin(opts.pin, &defaults),
pin_fn,
piv,
debug,
quiet,
protection,
resolved: RefCell::new(None),
flash_handle,
})
}
pub fn with_backend(
backend: Arc<dyn PivBackend>,
pin: Option<String>,
debug: bool,
) -> Result<Self> {
let devices = backend
.list_devices()
.context("listing devices in backend")?;
let device = match devices.as_slice() {
[] => bail!("no device found in backend"),
[d] => d.clone(),
_ => bail!("multiple devices in backend — use Context::new with --serial"),
};
let reader = device.reader.clone();
let (defaults, protection) = probe_card(&reader, backend.as_ref());
Ok(Self {
reader,
serial: device.serial,
firmware: device.version.clone(),
management_key: None,
defaults,
allow_defaults: false,
pin: initial_pin(pin, &defaults),
pin_fn: Box::new(|| Ok(None)),
piv: backend,
debug,
quiet: false,
protection,
resolved: RefCell::new(None),
flash_handle: None,
})
}
pub fn require_pin(&self) -> Result<Option<String>> {
if self.pin.borrow().is_some() {
return Ok(self.pin.borrow().as_ref().map(|z| z.as_str().to_owned()));
}
let resolved = (self.pin_fn)()?;
*self.pin.borrow_mut() = resolved.as_deref().map(|s| Zeroizing::new(s.to_owned()));
Ok(resolved)
}
pub fn known_pin(&self) -> Option<String> {
self.pin.borrow().as_ref().map(|z| z.as_str().to_owned())
}
pub fn set_pin(&self, pin: &str) {
*self.pin.borrow_mut() = Some(Zeroizing::new(pin.to_owned()));
}
pub fn refresh_defaults(&mut self) {
self.defaults = probe_card(&self.reader, self.piv.as_ref()).0;
}
pub fn enforce_default_policy(&self, op: SecretOp) -> Result<Vec<String>> {
let warnings = policy::default_policy(op, &self.defaults, self.allow_defaults)?;
if !self.quiet {
for warning in &warnings {
eprintln!("{warning}");
}
}
Ok(warnings)
}
pub fn ensure_supported_protection(&self) -> Result<()> {
match self.protection {
ProtectionMode::Derived => Err(auxiliaries::pin_derived_unsupported().into()),
ProtectionMode::Invalid => Err(YbError::new(
"the YubiKey's ADMIN DATA object (0x5FFF00) cannot be parsed",
)
.why("yb does not write to a YubiKey whose management key setup it cannot read")
.fix("inspect it with `ykman piv objects export 0x5fff00 -`")
.into()),
ProtectionMode::None
| ProtectionMode::Standard
| ProtectionMode::LegacyOrPukBlocked => Ok(()),
}
}
pub fn management_key_for_write(&self) -> Result<String> {
if let Some(r) = self.resolved.borrow().as_ref() {
return Ok(r.key.to_string());
}
self.ensure_supported_protection()?;
let resolved = self.resolve_management_key()?;
let key = resolved.key.to_string();
*self.resolved.borrow_mut() = Some(resolved);
Ok(key)
}
fn resolve_management_key(&self) -> Result<ResolvedKey> {
let piv = self.piv.as_ref();
let accepts = |key: &str| piv.authenticate_management_key(&self.reader, key).is_ok();
if let Some(ref key) = self.management_key {
piv.authenticate_management_key(&self.reader, key)
.context("the YubiKey rejected the management key from YB_MANAGEMENT_KEY")?;
return Ok(ResolvedKey::new(
key,
KeySource::Explicit,
Repairs::default(),
));
}
let printed = match self.require_pin()? {
Some(pin) => {
piv.verify_pin(&self.reader, &pin)?;
auxiliaries::read_printed_keys(&self.reader, piv, &pin)?
}
None if matches!(
self.protection,
ProtectionMode::Standard | ProtectionMode::LegacyOrPukBlocked
) =>
{
return Err(YbError::new(
"a PIN is needed to read the management key kept on the YubiKey",
)
.fix("set YB_PIN, use --pin-stdin, or run yb in a terminal")
.into())
}
None => auxiliaries::PrintedKeys::default(),
};
let has_printed_keys = printed.current.is_some() || printed.previous.is_some();
let candidates = [
(printed.current.as_deref(), KeySource::Printed),
(printed.previous.as_deref(), KeySource::PrintedPrevious),
(
Some(auxiliaries::DEFAULT_MANAGEMENT_KEY),
KeySource::FactoryDefault,
),
];
for (key, source) in candidates {
let Some(key) = key.filter(|k| accepts(k)) else {
continue;
};
let repairs = if key == auxiliaries::DEFAULT_MANAGEMENT_KEY {
Repairs {
flags: false,
printed: if has_printed_keys {
PrintedRepair::Delete
} else {
PrintedRepair::None
},
}
} else {
Repairs {
flags: self.protection != ProtectionMode::Standard,
printed: if printed.previous.is_some() {
PrintedRepair::Rewrite
} else {
PrintedRepair::None
},
}
};
return Ok(ResolvedKey::new(key, source, repairs));
}
Err(YbError::new(MANAGEMENT_KEY_NOT_FOUND)
.fix("set YB_MANAGEMENT_KEY")
.into())
}
pub fn management_key_protected(&self) -> Result<bool> {
let key = self.management_key_for_write()?;
if key.eq_ignore_ascii_case(auxiliaries::DEFAULT_MANAGEMENT_KEY) {
return Ok(false);
}
match self.management_key_source() {
Some(KeySource::Printed) => Ok(true),
Some(KeySource::Explicit) => {
let Some(pin) = self.require_pin()? else {
return Ok(false);
};
self.piv.verify_pin(&self.reader, &pin)?;
let printed =
auxiliaries::read_printed_keys(&self.reader, self.piv.as_ref(), &pin)?;
let protected = printed
.current
.as_deref()
.is_some_and(|k| k.eq_ignore_ascii_case(&key));
if protected {
if let Some(r) = self.resolved.borrow_mut().as_mut() {
r.repairs = Repairs {
flags: self.protection != ProtectionMode::Standard,
printed: if printed.previous.is_some() {
PrintedRepair::Rewrite
} else {
PrintedRepair::None
},
};
}
}
Ok(protected)
}
Some(KeySource::PrintedPrevious | KeySource::FactoryDefault) | None => Ok(false),
}
}
pub fn management_key_in_printed(&self) -> Result<bool> {
let key = self.management_key_for_write()?;
match self.management_key_source() {
Some(source) if source.is_printed() => Ok(true),
Some(KeySource::Explicit) => {
let Some(pin) = self.require_pin()? else {
return Ok(false);
};
self.piv.verify_pin(&self.reader, &pin)?;
let printed =
auxiliaries::read_printed_keys(&self.reader, self.piv.as_ref(), &pin)?;
Ok([printed.current, printed.previous]
.iter()
.flatten()
.any(|k| k.eq_ignore_ascii_case(&key)))
}
_ => Ok(false),
}
}
pub fn management_key_source(&self) -> Option<KeySource> {
self.resolved.borrow().as_ref().map(|r| r.source)
}
pub fn pending_repairs(&self) -> Repairs {
self.resolved
.borrow()
.as_ref()
.map(|r| r.repairs)
.unwrap_or_default()
}
pub fn complete_pending_repairs(&self) {
let (key, repairs) = match self.resolved.borrow_mut().as_mut() {
Some(r) => (r.key.to_string(), std::mem::take(&mut r.repairs)),
None => return,
};
let (reader, piv) = (self.reader.as_str(), self.piv.as_ref());
let note = |msg: &str| {
if !self.quiet {
eprintln!("yb: note: {msg}");
}
};
if repairs.flags {
match auxiliaries::admin_data_with_stored_key(reader, piv, true)
.and_then(|data| piv.write_object(reader, auxiliaries::OBJ_ADMIN_DATA, &data, &key))
{
Ok(()) if self.protection == ProtectionMode::LegacyOrPukBlocked => note(
"upgraded the stored management key metadata \
to the standard (ykman-compatible) layout",
),
Ok(()) => note(
"repaired the stored management key metadata \
after an interrupted key switch",
),
Err(e) => {
eprintln!("Warning: could not update the stored management key metadata: {e:#}")
}
}
}
let printed = match repairs.printed {
PrintedRepair::None => return,
PrintedRepair::Rewrite => auxiliaries::encode_printed(&key, None),
PrintedRepair::Delete => Ok(Vec::new()),
};
match printed
.and_then(|data| piv.write_object(reader, auxiliaries::OBJ_PRINTED, &data, &key))
{
Ok(()) => note("removed a management key left in PRINTED by an interrupted key switch"),
Err(e) => eprintln!("Warning: could not clean up PRINTED: {e:#}"),
}
}
pub fn check_slot_key(&self, slot: u8) -> Result<SlotKeyCheck> {
use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey};
let Ok(cert_der) = self.piv.read_certificate(&self.reader, slot) else {
return Ok(SlotKeyCheck::NoCertificate);
};
let public_key = parse_ec_public_key_from_cert_der(&cert_der).with_context(|| {
format!("the certificate in slot 0x{slot:02x} does not hold an EC P-256 key")
})?;
let digest: [u8; 32] = rand::random();
let pin = self.require_pin()?;
let raw = self
.piv
.ecdsa_sign(&self.reader, slot, &digest, pin.as_deref())
.with_context(|| format!("signing with the key in slot 0x{slot:02x}"))?;
let signature = Signature::from_slice(&raw).context("parsing the slot signature")?;
let matches = VerifyingKey::from(&public_key)
.verify_prehash(&digest, &signature)
.is_ok();
Ok(if matches {
SlotKeyCheck::Match
} else {
SlotKeyCheck::Mismatch
})
}
pub fn take_flash(&mut self) -> Option<Box<dyn crate::piv::FlashHandle>> {
self.flash_handle.take()
}
pub fn get_public_key(&self, slot: u8) -> Result<PublicKey> {
let cert_der = self
.piv
.read_certificate(&self.reader, slot)
.with_context(|| format!("reading certificate from slot 0x{slot:02x}"))?;
parse_ec_public_key_from_cert_der(&cert_der)
}
}
fn probe_card(
reader: &str,
piv: &dyn PivBackend,
) -> (auxiliaries::DefaultCredentials, ProtectionMode) {
let defaults = if std::env::var("YB_SKIP_DEFAULT_CHECK").is_ok() {
auxiliaries::DefaultCredentials::default()
} else {
auxiliaries::detect_default_credentials(reader, piv)
};
(defaults, auxiliaries::detect_protection_mode(reader, piv))
}
fn initial_pin(
explicit: Option<String>,
defaults: &auxiliaries::DefaultCredentials,
) -> RefCell<Option<Zeroizing<String>>> {
let pin = explicit.or_else(|| defaults.pin.then(|| auxiliaries::DEFAULT_PIN.to_owned()));
RefCell::new(pin.map(Zeroizing::new))
}
#[allow(clippy::type_complexity)]
fn select_device(
devices: &[DeviceInfo],
serial: Option<&u32>,
reader: Option<&str>,
piv: &Arc<dyn PivBackend>,
device_picker: &dyn Fn(
&Arc<dyn PivBackend>,
&[DeviceInfo],
) -> Result<
Option<(DeviceInfo, Option<Box<dyn crate::piv::FlashHandle>>)>,
>,
) -> Result<(DeviceInfo, String, Option<Box<dyn crate::piv::FlashHandle>>)> {
if let Some(s) = serial {
let dev = devices.iter().find(|d| &d.serial == s).ok_or_else(|| {
YbError::new("no connected YubiKey has the requested serial number")
.fix("`ykman list --serials` lists the connected YubiKeys")
})?;
return Ok((dev.clone(), dev.reader.clone(), None));
}
if let Some(r) = reader {
let dev = devices.iter().find(|d| d.reader == r).ok_or_else(|| {
YbError::new("no YubiKey on the requested reader")
.fix("`yb list-readers` lists the readers")
})?;
return Ok((dev.clone(), r.to_owned(), None));
}
match devices.len() {
0 => Err(CardError::Pcsc {
op: PcscOp::ListReaders,
code: PcscCode::NoReaders,
}
.into()),
1 => Ok((devices[0].clone(), devices[0].reader.clone(), None)),
_ => {
match device_picker(piv, devices)? {
Some((dev, flash)) => {
let reader = dev.reader.clone();
Ok((dev, reader, flash))
}
None => bail!("device selection cancelled"),
}
}
}
}
pub fn parse_ec_public_key_from_cert_der(cert_der: &[u8]) -> Result<PublicKey> {
use der::Decode;
use p256::elliptic_curve::sec1::FromEncodedPoint;
use x509_cert::Certificate;
let cert = Certificate::from_der(cert_der).context("parsing DER certificate")?;
let spki = &cert.tbs_certificate.subject_public_key_info;
let point_bytes = spki.subject_public_key.as_bytes().ok_or_else(|| {
anyhow::anyhow!("certificate SubjectPublicKeyInfo: unexpected bit-string encoding")
})?;
let encoded = p256::EncodedPoint::from_bytes(point_bytes)
.map_err(|e| anyhow::anyhow!("parsing EC point from SPKI: {e}"))?;
let pk: Option<p256::PublicKey> = p256::PublicKey::from_encoded_point(&encoded).into();
pk.ok_or_else(|| anyhow::anyhow!("EC point in certificate is not on P-256 curve"))
}
#[cfg(all(test, feature = "virtual-piv"))]
mod tests {
use super::*;
#[test]
fn skip_default_check_disables_detection() {
let piv: Arc<dyn PivBackend> = Arc::new(VirtualPiv::new());
let ctx = Context::with_backend(piv.clone(), None, false).unwrap();
assert!(ctx.defaults.any());
assert_eq!(ctx.require_pin().unwrap().as_deref(), Some("123456"));
std::env::set_var("YB_SKIP_DEFAULT_CHECK", "1");
let ctx = Context::with_backend(piv, None, false);
std::env::remove_var("YB_SKIP_DEFAULT_CHECK");
let ctx = ctx.unwrap();
assert!(!ctx.defaults.any());
assert_eq!(ctx.require_pin().unwrap(), None, "no PIN filled in");
assert!(ctx
.enforce_default_policy(SecretOp::Store)
.unwrap()
.is_empty());
}
}