1#[cfg(feature = "virtual-piv")]
8use crate::piv::VirtualPiv;
9use crate::{
10 auxiliaries::{self, ProtectionMode},
11 errors::{CardError, PcscCode, PcscOp, YbError},
12 piv::{hardware::HardwarePiv, DeviceInfo, PivBackend},
13 policy::{self, SecretOp},
14};
15use anyhow::{bail, Context as _, Result};
16use p256::PublicKey;
17use std::cell::RefCell;
18use std::sync::Arc;
19use zeroize::Zeroizing;
20
21#[derive(Debug, Clone, Copy, Default)]
23pub struct OutputOptions {
24 pub debug: bool,
25 pub quiet: bool,
26}
27
28pub type DevicePicker = Box<
34 dyn Fn(
35 &Arc<dyn PivBackend>,
36 &[DeviceInfo],
37 ) -> Result<Option<(DeviceInfo, Option<Box<dyn crate::piv::FlashHandle>>)>>,
38>;
39
40#[derive(Debug, Default)]
42pub struct ContextOptions {
43 pub serial: Option<u32>,
44 pub reader: Option<String>,
45 pub management_key: Option<String>,
46 pub pin: Option<String>,
47 pub allow_defaults: bool,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum KeySource {
54 Explicit,
56 Printed,
58 PrintedPrevious,
60 FactoryDefault,
62}
63
64impl KeySource {
65 pub fn is_printed(self) -> bool {
67 matches!(self, Self::Printed | Self::PrintedPrevious)
68 }
69}
70
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
73pub enum PrintedRepair {
74 #[default]
75 None,
76 Rewrite,
78 Delete,
80}
81
82#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
84pub struct Repairs {
85 pub flags: bool,
88 pub printed: PrintedRepair,
89}
90
91impl Repairs {
92 pub fn any(&self) -> bool {
93 self.flags || self.printed != PrintedRepair::None
94 }
95}
96
97pub const MANAGEMENT_KEY_NOT_FOUND: &str =
100 "the management key is not in PRINTED and is not the factory default";
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum SlotKeyCheck {
105 NoCertificate,
106 Match,
107 Mismatch,
108}
109
110struct ResolvedKey {
111 key: Zeroizing<String>,
112 source: KeySource,
113 repairs: Repairs,
114}
115
116impl ResolvedKey {
117 fn new(key: &str, source: KeySource, repairs: Repairs) -> Self {
118 Self {
119 key: Zeroizing::new(key.to_owned()),
120 source,
121 repairs,
122 }
123 }
124}
125
126pub struct Context {
127 pub reader: String,
128 pub serial: u32,
129 pub firmware: String,
132 pub management_key: Option<String>,
133 pub defaults: auxiliaries::DefaultCredentials,
135 pub allow_defaults: bool,
138 pin: RefCell<Option<Zeroizing<String>>>,
142 pin_fn: Box<dyn Fn() -> Result<Option<String>>>,
145 pub piv: Arc<dyn PivBackend>,
146 pub debug: bool,
147 pub quiet: bool,
148 pub protection: ProtectionMode,
150 resolved: RefCell<Option<ResolvedKey>>,
153 pub flash_handle: Option<Box<dyn crate::piv::FlashHandle>>,
157}
158
159impl Context {
160 pub fn new(
167 opts: ContextOptions,
168 pin_fn: Box<dyn Fn() -> Result<Option<String>>>,
169 device_picker: DevicePicker,
170 output: OutputOptions,
171 ) -> Result<Self> {
172 let debug = output.debug;
173 let quiet = output.quiet;
174 #[cfg(feature = "virtual-piv")]
175 let piv: Arc<dyn PivBackend> = if let Ok(path) = std::env::var("YB_FIXTURE") {
176 Arc::new(VirtualPiv::from_fixture(std::path::Path::new(&path))?)
177 } else {
178 Arc::new(HardwarePiv::new())
179 };
180 #[cfg(not(feature = "virtual-piv"))]
181 let piv: Arc<dyn PivBackend> = Arc::new(HardwarePiv::new());
182
183 let devices = piv.list_devices().context("listing YubiKey devices")?;
184
185 let (device, selected_reader, flash_handle) = select_device(
186 &devices,
187 opts.serial.as_ref(),
188 opts.reader.as_deref(),
189 &piv,
190 &*device_picker,
191 )?;
192
193 let (defaults, protection) = probe_card(&selected_reader, piv.as_ref());
194
195 Ok(Self {
196 reader: selected_reader,
197 serial: device.serial,
198 firmware: device.version.clone(),
199 management_key: opts.management_key,
200 defaults,
201 allow_defaults: opts.allow_defaults,
202 pin: initial_pin(opts.pin, &defaults),
203 pin_fn,
204 piv,
205 debug,
206 quiet,
207 protection,
208 resolved: RefCell::new(None),
209 flash_handle,
210 })
211 }
212
213 pub fn with_backend(
222 backend: Arc<dyn PivBackend>,
223 pin: Option<String>,
224 debug: bool,
225 ) -> Result<Self> {
226 let devices = backend
227 .list_devices()
228 .context("listing devices in backend")?;
229 let device = match devices.as_slice() {
230 [] => bail!("no device found in backend"),
231 [d] => d.clone(),
232 _ => bail!("multiple devices in backend — use Context::new with --serial"),
233 };
234 let reader = device.reader.clone();
235 let (defaults, protection) = probe_card(&reader, backend.as_ref());
236
237 Ok(Self {
238 reader,
239 serial: device.serial,
240 firmware: device.version.clone(),
241 management_key: None,
242 defaults,
243 allow_defaults: false,
244 pin: initial_pin(pin, &defaults),
245 pin_fn: Box::new(|| Ok(None)),
246 piv: backend,
247 debug,
248 quiet: false,
249 protection,
250 resolved: RefCell::new(None),
251 flash_handle: None,
252 })
253 }
254
255 pub fn require_pin(&self) -> Result<Option<String>> {
264 if self.pin.borrow().is_some() {
265 return Ok(self.pin.borrow().as_ref().map(|z| z.as_str().to_owned()));
266 }
267 let resolved = (self.pin_fn)()?;
268 *self.pin.borrow_mut() = resolved.as_deref().map(|s| Zeroizing::new(s.to_owned()));
269 Ok(resolved)
270 }
271
272 pub fn known_pin(&self) -> Option<String> {
275 self.pin.borrow().as_ref().map(|z| z.as_str().to_owned())
276 }
277
278 pub fn set_pin(&self, pin: &str) {
280 *self.pin.borrow_mut() = Some(Zeroizing::new(pin.to_owned()));
281 }
282
283 pub fn refresh_defaults(&mut self) {
286 self.defaults = probe_card(&self.reader, self.piv.as_ref()).0;
287 }
288
289 pub fn enforce_default_policy(&self, op: SecretOp) -> Result<Vec<String>> {
293 let warnings = policy::default_policy(op, &self.defaults, self.allow_defaults)?;
294 if !self.quiet {
295 for warning in &warnings {
296 eprintln!("{warning}");
297 }
298 }
299 Ok(warnings)
300 }
301
302 pub fn ensure_supported_protection(&self) -> Result<()> {
305 match self.protection {
306 ProtectionMode::Derived => Err(auxiliaries::pin_derived_unsupported().into()),
307 ProtectionMode::Invalid => Err(YbError::new(
308 "the YubiKey's ADMIN DATA object (0x5FFF00) cannot be parsed",
309 )
310 .why("yb does not write to a YubiKey whose management key setup it cannot read")
311 .fix("inspect it with `ykman piv objects export 0x5fff00 -`")
312 .into()),
313 ProtectionMode::None
314 | ProtectionMode::Standard
315 | ProtectionMode::LegacyOrPukBlocked => Ok(()),
316 }
317 }
318
319 pub fn management_key_for_write(&self) -> Result<String> {
333 if let Some(r) = self.resolved.borrow().as_ref() {
334 return Ok(r.key.to_string());
335 }
336 self.ensure_supported_protection()?;
337 let resolved = self.resolve_management_key()?;
338 let key = resolved.key.to_string();
339 *self.resolved.borrow_mut() = Some(resolved);
340 Ok(key)
341 }
342
343 fn resolve_management_key(&self) -> Result<ResolvedKey> {
344 let piv = self.piv.as_ref();
345 let accepts = |key: &str| piv.authenticate_management_key(&self.reader, key).is_ok();
346
347 if let Some(ref key) = self.management_key {
348 piv.authenticate_management_key(&self.reader, key)
349 .context("the YubiKey rejected the management key from YB_MANAGEMENT_KEY")?;
350 return Ok(ResolvedKey::new(
351 key,
352 KeySource::Explicit,
353 Repairs::default(),
354 ));
355 }
356
357 let printed = match self.require_pin()? {
358 Some(pin) => {
359 piv.verify_pin(&self.reader, &pin)?;
362 auxiliaries::read_printed_keys(&self.reader, piv, &pin)?
363 }
364 None if matches!(
365 self.protection,
366 ProtectionMode::Standard | ProtectionMode::LegacyOrPukBlocked
367 ) =>
368 {
369 return Err(YbError::new(
370 "a PIN is needed to read the management key kept on the YubiKey",
371 )
372 .fix("set YB_PIN, use --pin-stdin, or run yb in a terminal")
373 .into())
374 }
375 None => auxiliaries::PrintedKeys::default(),
376 };
377 let has_printed_keys = printed.current.is_some() || printed.previous.is_some();
378
379 let candidates = [
380 (printed.current.as_deref(), KeySource::Printed),
381 (printed.previous.as_deref(), KeySource::PrintedPrevious),
382 (
383 Some(auxiliaries::DEFAULT_MANAGEMENT_KEY),
384 KeySource::FactoryDefault,
385 ),
386 ];
387 for (key, source) in candidates {
388 let Some(key) = key.filter(|k| accepts(k)) else {
389 continue;
390 };
391 let repairs = if key == auxiliaries::DEFAULT_MANAGEMENT_KEY {
392 Repairs {
396 flags: false,
397 printed: if has_printed_keys {
398 PrintedRepair::Delete
399 } else {
400 PrintedRepair::None
401 },
402 }
403 } else {
404 Repairs {
405 flags: self.protection != ProtectionMode::Standard,
406 printed: if printed.previous.is_some() {
407 PrintedRepair::Rewrite
408 } else {
409 PrintedRepair::None
410 },
411 }
412 };
413 return Ok(ResolvedKey::new(key, source, repairs));
414 }
415 Err(YbError::new(MANAGEMENT_KEY_NOT_FOUND)
416 .fix("set YB_MANAGEMENT_KEY")
417 .into())
418 }
419
420 pub fn management_key_protected(&self) -> Result<bool> {
429 let key = self.management_key_for_write()?;
430 if key.eq_ignore_ascii_case(auxiliaries::DEFAULT_MANAGEMENT_KEY) {
431 return Ok(false);
432 }
433 match self.management_key_source() {
434 Some(KeySource::Printed) => Ok(true),
435 Some(KeySource::Explicit) => {
436 let Some(pin) = self.require_pin()? else {
437 return Ok(false);
438 };
439 self.piv.verify_pin(&self.reader, &pin)?;
440 let printed =
441 auxiliaries::read_printed_keys(&self.reader, self.piv.as_ref(), &pin)?;
442 let protected = printed
443 .current
444 .as_deref()
445 .is_some_and(|k| k.eq_ignore_ascii_case(&key));
446 if protected {
447 if let Some(r) = self.resolved.borrow_mut().as_mut() {
448 r.repairs = Repairs {
449 flags: self.protection != ProtectionMode::Standard,
450 printed: if printed.previous.is_some() {
451 PrintedRepair::Rewrite
452 } else {
453 PrintedRepair::None
454 },
455 };
456 }
457 }
458 Ok(protected)
459 }
460 Some(KeySource::PrintedPrevious | KeySource::FactoryDefault) | None => Ok(false),
461 }
462 }
463
464 pub fn management_key_in_printed(&self) -> Result<bool> {
468 let key = self.management_key_for_write()?;
469 match self.management_key_source() {
470 Some(source) if source.is_printed() => Ok(true),
471 Some(KeySource::Explicit) => {
472 let Some(pin) = self.require_pin()? else {
473 return Ok(false);
474 };
475 self.piv.verify_pin(&self.reader, &pin)?;
476 let printed =
477 auxiliaries::read_printed_keys(&self.reader, self.piv.as_ref(), &pin)?;
478 Ok([printed.current, printed.previous]
479 .iter()
480 .flatten()
481 .any(|k| k.eq_ignore_ascii_case(&key)))
482 }
483 _ => Ok(false),
484 }
485 }
486
487 pub fn management_key_source(&self) -> Option<KeySource> {
489 self.resolved.borrow().as_ref().map(|r| r.source)
490 }
491
492 pub fn pending_repairs(&self) -> Repairs {
494 self.resolved
495 .borrow()
496 .as_ref()
497 .map(|r| r.repairs)
498 .unwrap_or_default()
499 }
500
501 pub fn complete_pending_repairs(&self) {
508 let (key, repairs) = match self.resolved.borrow_mut().as_mut() {
509 Some(r) => (r.key.to_string(), std::mem::take(&mut r.repairs)),
510 None => return,
511 };
512 let (reader, piv) = (self.reader.as_str(), self.piv.as_ref());
513 let note = |msg: &str| {
514 if !self.quiet {
515 eprintln!("yb: note: {msg}");
516 }
517 };
518
519 if repairs.flags {
520 match auxiliaries::admin_data_with_stored_key(reader, piv, true)
521 .and_then(|data| piv.write_object(reader, auxiliaries::OBJ_ADMIN_DATA, &data, &key))
522 {
523 Ok(()) if self.protection == ProtectionMode::LegacyOrPukBlocked => note(
524 "upgraded the stored management key metadata \
525 to the standard (ykman-compatible) layout",
526 ),
527 Ok(()) => note(
528 "repaired the stored management key metadata \
529 after an interrupted key switch",
530 ),
531 Err(e) => {
532 eprintln!("Warning: could not update the stored management key metadata: {e:#}")
533 }
534 }
535 }
536
537 let printed = match repairs.printed {
538 PrintedRepair::None => return,
539 PrintedRepair::Rewrite => auxiliaries::encode_printed(&key, None),
540 PrintedRepair::Delete => Ok(Vec::new()),
541 };
542 match printed
543 .and_then(|data| piv.write_object(reader, auxiliaries::OBJ_PRINTED, &data, &key))
544 {
545 Ok(()) => note("removed a management key left in PRINTED by an interrupted key switch"),
546 Err(e) => eprintln!("Warning: could not clean up PRINTED: {e:#}"),
547 }
548 }
549
550 pub fn check_slot_key(&self, slot: u8) -> Result<SlotKeyCheck> {
554 use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey};
555
556 let Ok(cert_der) = self.piv.read_certificate(&self.reader, slot) else {
557 return Ok(SlotKeyCheck::NoCertificate);
558 };
559 let public_key = parse_ec_public_key_from_cert_der(&cert_der).with_context(|| {
560 format!("the certificate in slot 0x{slot:02x} does not hold an EC P-256 key")
561 })?;
562 let digest: [u8; 32] = rand::random();
563 let pin = self.require_pin()?;
564 let raw = self
565 .piv
566 .ecdsa_sign(&self.reader, slot, &digest, pin.as_deref())
567 .with_context(|| format!("signing with the key in slot 0x{slot:02x}"))?;
568 let signature = Signature::from_slice(&raw).context("parsing the slot signature")?;
569 let matches = VerifyingKey::from(&public_key)
570 .verify_prehash(&digest, &signature)
571 .is_ok();
572 Ok(if matches {
573 SlotKeyCheck::Match
574 } else {
575 SlotKeyCheck::Mismatch
576 })
577 }
578
579 pub fn take_flash(&mut self) -> Option<Box<dyn crate::piv::FlashHandle>> {
585 self.flash_handle.take()
586 }
587
588 pub fn get_public_key(&self, slot: u8) -> Result<PublicKey> {
590 let cert_der = self
591 .piv
592 .read_certificate(&self.reader, slot)
593 .with_context(|| format!("reading certificate from slot 0x{slot:02x}"))?;
594 parse_ec_public_key_from_cert_der(&cert_der)
595 }
596}
597
598fn probe_card(
608 reader: &str,
609 piv: &dyn PivBackend,
610) -> (auxiliaries::DefaultCredentials, ProtectionMode) {
611 let defaults = if std::env::var("YB_SKIP_DEFAULT_CHECK").is_ok() {
612 auxiliaries::DefaultCredentials::default()
613 } else {
614 auxiliaries::detect_default_credentials(reader, piv)
615 };
616 (defaults, auxiliaries::detect_protection_mode(reader, piv))
617}
618
619fn initial_pin(
622 explicit: Option<String>,
623 defaults: &auxiliaries::DefaultCredentials,
624) -> RefCell<Option<Zeroizing<String>>> {
625 let pin = explicit.or_else(|| defaults.pin.then(|| auxiliaries::DEFAULT_PIN.to_owned()));
626 RefCell::new(pin.map(Zeroizing::new))
627}
628
629#[allow(clippy::type_complexity)]
634fn select_device(
635 devices: &[DeviceInfo],
636 serial: Option<&u32>,
637 reader: Option<&str>,
638 piv: &Arc<dyn PivBackend>,
639 device_picker: &dyn Fn(
640 &Arc<dyn PivBackend>,
641 &[DeviceInfo],
642 ) -> Result<
643 Option<(DeviceInfo, Option<Box<dyn crate::piv::FlashHandle>>)>,
644 >,
645) -> Result<(DeviceInfo, String, Option<Box<dyn crate::piv::FlashHandle>>)> {
646 if let Some(s) = serial {
647 let dev = devices.iter().find(|d| &d.serial == s).ok_or_else(|| {
648 YbError::new("no connected YubiKey has the requested serial number")
650 .fix("`ykman list --serials` lists the connected YubiKeys")
651 })?;
652 return Ok((dev.clone(), dev.reader.clone(), None));
653 }
654
655 if let Some(r) = reader {
656 let dev = devices.iter().find(|d| d.reader == r).ok_or_else(|| {
657 YbError::new("no YubiKey on the requested reader")
658 .fix("`yb list-readers` lists the readers")
659 })?;
660 return Ok((dev.clone(), r.to_owned(), None));
661 }
662
663 match devices.len() {
664 0 => Err(CardError::Pcsc {
665 op: PcscOp::ListReaders,
666 code: PcscCode::NoReaders,
667 }
668 .into()),
669 1 => Ok((devices[0].clone(), devices[0].reader.clone(), None)),
670 _ => {
671 match device_picker(piv, devices)? {
673 Some((dev, flash)) => {
674 let reader = dev.reader.clone();
675 Ok((dev, reader, flash))
676 }
677 None => bail!("device selection cancelled"),
678 }
679 }
680 }
681}
682
683pub fn parse_ec_public_key_from_cert_der(cert_der: &[u8]) -> Result<PublicKey> {
689 use der::Decode;
690 use p256::elliptic_curve::sec1::FromEncodedPoint;
691 use x509_cert::Certificate;
692
693 let cert = Certificate::from_der(cert_der).context("parsing DER certificate")?;
694
695 let spki = &cert.tbs_certificate.subject_public_key_info;
697 let point_bytes = spki.subject_public_key.as_bytes().ok_or_else(|| {
698 anyhow::anyhow!("certificate SubjectPublicKeyInfo: unexpected bit-string encoding")
699 })?;
700
701 let encoded = p256::EncodedPoint::from_bytes(point_bytes)
702 .map_err(|e| anyhow::anyhow!("parsing EC point from SPKI: {e}"))?;
703 let pk: Option<p256::PublicKey> = p256::PublicKey::from_encoded_point(&encoded).into();
704 pk.ok_or_else(|| anyhow::anyhow!("EC point in certificate is not on P-256 curve"))
705}
706
707#[cfg(all(test, feature = "virtual-piv"))]
712mod tests {
713 use super::*;
714
715 #[test]
719 fn skip_default_check_disables_detection() {
720 let piv: Arc<dyn PivBackend> = Arc::new(VirtualPiv::new());
722 let ctx = Context::with_backend(piv.clone(), None, false).unwrap();
723 assert!(ctx.defaults.any());
724 assert_eq!(ctx.require_pin().unwrap().as_deref(), Some("123456"));
725
726 std::env::set_var("YB_SKIP_DEFAULT_CHECK", "1");
727 let ctx = Context::with_backend(piv, None, false);
728 std::env::remove_var("YB_SKIP_DEFAULT_CHECK");
729 let ctx = ctx.unwrap();
730 assert!(!ctx.defaults.any());
731 assert_eq!(ctx.require_pin().unwrap(), None, "no PIN filled in");
732 assert!(ctx
733 .enforce_default_policy(SecretOp::Store)
734 .unwrap()
735 .is_empty());
736 }
737}