1use crate::{
8 auxiliaries::{self, ProtectionMode},
9 errors::{CardError, PcscCode, PcscOp, YbError},
10 piv::{DeviceInfo, PivBackend},
11 policy::{self, SecretOp},
12};
13use anyhow::{bail, Context as _, Result};
14use p256::PublicKey;
15use std::cell::RefCell;
16use std::sync::Arc;
17use zeroize::Zeroizing;
18
19#[derive(Debug, Clone, Copy, Default)]
21pub struct OutputOptions {
22 pub debug: bool,
23 pub quiet: bool,
24}
25
26pub type DevicePicker = Box<
32 dyn Fn(
33 &Arc<dyn PivBackend>,
34 &[DeviceInfo],
35 ) -> Result<Option<(DeviceInfo, Option<Box<dyn crate::piv::FlashHandle>>)>>,
36>;
37
38#[derive(Debug, Default)]
40pub struct ContextOptions {
41 pub serial: Option<u32>,
42 pub reader: Option<String>,
43 pub management_key: Option<Zeroizing<String>>,
44 pub pin: Option<String>,
45 pub allow_defaults: bool,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum KeySource {
52 Explicit,
54 Printed,
56 PrintedPrevious,
58 FactoryDefault,
60}
61
62impl KeySource {
63 pub fn is_printed(self) -> bool {
65 matches!(self, Self::Printed | Self::PrintedPrevious)
66 }
67}
68
69#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub enum PrintedRepair {
72 #[default]
73 None,
74 Rewrite,
76 Delete,
78}
79
80#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
82pub struct Repairs {
83 pub flags: bool,
86 pub printed: PrintedRepair,
87}
88
89impl Repairs {
90 pub fn any(&self) -> bool {
91 self.flags || self.printed != PrintedRepair::None
92 }
93}
94
95pub const MANAGEMENT_KEY_NOT_FOUND: &str =
98 "the management key is not in PRINTED and is not the factory default";
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum SlotKeyCheck {
103 NoCertificate,
104 Match,
105 Mismatch,
106}
107
108struct ResolvedKey {
109 key: Zeroizing<String>,
110 source: KeySource,
111 repairs: Repairs,
112}
113
114impl ResolvedKey {
115 fn new(key: &str, source: KeySource, repairs: Repairs) -> Self {
116 Self {
117 key: Zeroizing::new(key.to_owned()),
118 source,
119 repairs,
120 }
121 }
122}
123
124pub struct Context {
125 pub reader: String,
126 pub serial: u32,
127 pub firmware: String,
130 pub management_key: Option<Zeroizing<String>>,
132 pub defaults: auxiliaries::DefaultCredentials,
134 pub allow_defaults: bool,
137 pin: RefCell<Option<Zeroizing<String>>>,
141 pub pin_fn: Box<dyn Fn() -> Result<Option<String>>>,
144 pub piv: Arc<dyn PivBackend>,
145 pub debug: bool,
146 pub quiet: bool,
147 pub protection: ProtectionMode,
149 resolved: RefCell<Option<ResolvedKey>>,
152 pub flash_handle: Option<Box<dyn crate::piv::FlashHandle>>,
156}
157
158impl Context {
159 pub fn new(
166 opts: ContextOptions,
167 pin_fn: Box<dyn Fn() -> Result<Option<String>>>,
168 device_picker: DevicePicker,
169 output: OutputOptions,
170 ) -> Result<Self> {
171 let debug = output.debug;
172 let quiet = output.quiet;
173 let piv = crate::piv::open_backend()?;
174
175 let devices = piv.list_devices().context("listing YubiKey devices")?;
176
177 let (device, selected_reader, flash_handle) = select_device(
178 &devices,
179 opts.serial.as_ref(),
180 opts.reader.as_deref(),
181 &piv,
182 &*device_picker,
183 )?;
184
185 let (defaults, protection) = probe_card(&selected_reader, piv.as_ref());
186
187 Ok(Self {
188 reader: selected_reader,
189 serial: device.serial,
190 firmware: device.version.clone(),
191 management_key: opts.management_key,
192 defaults,
193 allow_defaults: opts.allow_defaults,
194 pin: initial_pin(opts.pin, &defaults),
195 pin_fn,
196 piv,
197 debug,
198 quiet,
199 protection,
200 resolved: RefCell::new(None),
201 flash_handle,
202 })
203 }
204
205 pub fn with_backend(
214 backend: Arc<dyn PivBackend>,
215 pin: Option<String>,
216 debug: bool,
217 ) -> Result<Self> {
218 let devices = backend
219 .list_devices()
220 .context("listing devices in backend")?;
221 let device = match devices.as_slice() {
222 [] => bail!("no device found in backend"),
223 [d] => d.clone(),
224 _ => bail!("multiple devices in backend — use Context::new with --serial"),
225 };
226 let reader = device.reader.clone();
227 let (defaults, protection) = probe_card(&reader, backend.as_ref());
228
229 Ok(Self {
230 reader,
231 serial: device.serial,
232 firmware: device.version.clone(),
233 management_key: None,
234 defaults,
235 allow_defaults: false,
236 pin: initial_pin(pin, &defaults),
237 pin_fn: Box::new(|| Ok(None)),
238 piv: backend,
239 debug,
240 quiet: false,
241 protection,
242 resolved: RefCell::new(None),
243 flash_handle: None,
244 })
245 }
246
247 pub fn require_pin(&self) -> Result<Option<String>> {
256 if self.pin.borrow().is_some() {
257 return Ok(self.pin.borrow().as_ref().map(|z| z.as_str().to_owned()));
258 }
259 let resolved = (self.pin_fn)()?;
260 *self.pin.borrow_mut() = resolved.as_deref().map(|s| Zeroizing::new(s.to_owned()));
261 Ok(resolved)
262 }
263
264 pub fn known_pin(&self) -> Option<String> {
267 self.pin.borrow().as_ref().map(|z| z.as_str().to_owned())
268 }
269
270 pub fn set_pin(&self, pin: &str) {
272 *self.pin.borrow_mut() = Some(Zeroizing::new(pin.to_owned()));
273 }
274
275 pub fn clear_secrets(&self) {
279 *self.pin.borrow_mut() = None;
280 *self.resolved.borrow_mut() = None;
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<Zeroizing<String>> {
333 if let Some(r) = self.resolved.borrow().as_ref() {
334 return Ok(r.key.clone());
335 }
336 self.ensure_supported_protection()?;
337 let resolved = self.resolve_management_key()?;
338 let key = resolved.key.clone();
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 (
381 printed.current.as_ref().map(|k| k.as_str()),
382 KeySource::Printed,
383 ),
384 (
385 printed.previous.as_ref().map(|k| k.as_str()),
386 KeySource::PrintedPrevious,
387 ),
388 (
389 Some(auxiliaries::DEFAULT_MANAGEMENT_KEY),
390 KeySource::FactoryDefault,
391 ),
392 ];
393 for (key, source) in candidates {
394 let Some(key) = key.filter(|k| accepts(k)) else {
395 continue;
396 };
397 let repairs = if key == auxiliaries::DEFAULT_MANAGEMENT_KEY {
398 Repairs {
402 flags: false,
403 printed: if has_printed_keys {
404 PrintedRepair::Delete
405 } else {
406 PrintedRepair::None
407 },
408 }
409 } else {
410 Repairs {
411 flags: self.protection != ProtectionMode::Standard,
412 printed: if printed.previous.is_some() {
413 PrintedRepair::Rewrite
414 } else {
415 PrintedRepair::None
416 },
417 }
418 };
419 return Ok(ResolvedKey::new(key, source, repairs));
420 }
421 Err(YbError::new(MANAGEMENT_KEY_NOT_FOUND)
422 .fix("set YB_MANAGEMENT_KEY")
423 .into())
424 }
425
426 pub fn management_key_protected(&self) -> Result<bool> {
435 let key = self.management_key_for_write()?;
436 if key.eq_ignore_ascii_case(auxiliaries::DEFAULT_MANAGEMENT_KEY) {
437 return Ok(false);
438 }
439 match self.management_key_source() {
440 Some(KeySource::Printed) => Ok(true),
441 Some(KeySource::Explicit) => {
442 let Some(pin) = self.require_pin()? else {
443 return Ok(false);
444 };
445 self.piv.verify_pin(&self.reader, &pin)?;
446 let printed =
447 auxiliaries::read_printed_keys(&self.reader, self.piv.as_ref(), &pin)?;
448 let protected = printed
449 .current
450 .as_deref()
451 .is_some_and(|k| k.eq_ignore_ascii_case(&key));
452 if protected {
453 if let Some(r) = self.resolved.borrow_mut().as_mut() {
454 r.repairs = Repairs {
455 flags: self.protection != ProtectionMode::Standard,
456 printed: if printed.previous.is_some() {
457 PrintedRepair::Rewrite
458 } else {
459 PrintedRepair::None
460 },
461 };
462 }
463 }
464 Ok(protected)
465 }
466 Some(KeySource::PrintedPrevious | KeySource::FactoryDefault) | None => Ok(false),
467 }
468 }
469
470 pub fn management_key_in_printed(&self) -> Result<bool> {
474 let key = self.management_key_for_write()?;
475 match self.management_key_source() {
476 Some(source) if source.is_printed() => Ok(true),
477 Some(KeySource::Explicit) => {
478 let Some(pin) = self.require_pin()? else {
479 return Ok(false);
480 };
481 self.piv.verify_pin(&self.reader, &pin)?;
482 let printed =
483 auxiliaries::read_printed_keys(&self.reader, self.piv.as_ref(), &pin)?;
484 Ok([printed.current, printed.previous]
485 .iter()
486 .flatten()
487 .any(|k| k.eq_ignore_ascii_case(&key)))
488 }
489 _ => Ok(false),
490 }
491 }
492
493 pub fn management_key_source(&self) -> Option<KeySource> {
495 self.resolved.borrow().as_ref().map(|r| r.source)
496 }
497
498 pub fn pending_repairs(&self) -> Repairs {
500 self.resolved
501 .borrow()
502 .as_ref()
503 .map(|r| r.repairs)
504 .unwrap_or_default()
505 }
506
507 pub fn complete_pending_repairs(&self) {
514 let (key, repairs) = match self.resolved.borrow_mut().as_mut() {
515 Some(r) => (r.key.clone(), std::mem::take(&mut r.repairs)),
516 None => return,
517 };
518 let (reader, piv) = (self.reader.as_str(), self.piv.as_ref());
519 let note = |msg: &str| {
520 if !self.quiet {
521 eprintln!("yb: note: {msg}");
522 }
523 };
524
525 if repairs.flags {
526 match auxiliaries::admin_data_with_stored_key(reader, piv, true)
527 .and_then(|data| piv.write_object(reader, auxiliaries::OBJ_ADMIN_DATA, &data, &key))
528 {
529 Ok(()) if self.protection == ProtectionMode::LegacyOrPukBlocked => note(
530 "upgraded the stored management key metadata \
531 to the standard (ykman-compatible) layout",
532 ),
533 Ok(()) => note(
534 "repaired the stored management key metadata \
535 after an interrupted key switch",
536 ),
537 Err(e) => {
538 eprintln!("Warning: could not update the stored management key metadata: {e:#}")
539 }
540 }
541 }
542
543 let printed = match repairs.printed {
544 PrintedRepair::None => return,
545 PrintedRepair::Rewrite => auxiliaries::encode_printed(&key, None),
546 PrintedRepair::Delete => Ok(Zeroizing::new(Vec::new())),
547 };
548 match printed
549 .and_then(|data| piv.write_object(reader, auxiliaries::OBJ_PRINTED, &data, &key))
550 {
551 Ok(()) => note("removed a management key left in PRINTED by an interrupted key switch"),
552 Err(e) => eprintln!("Warning: could not clean up PRINTED: {e:#}"),
553 }
554 }
555
556 pub fn check_slot_key(&self, slot: u8) -> Result<SlotKeyCheck> {
560 use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey};
561
562 let Ok(cert_der) = self.piv.read_certificate(&self.reader, slot) else {
563 return Ok(SlotKeyCheck::NoCertificate);
564 };
565 let public_key = parse_ec_public_key_from_cert_der(&cert_der).with_context(|| {
566 format!("the certificate in slot 0x{slot:02x} does not hold an EC P-256 key")
567 })?;
568 let digest: [u8; 32] = rand::random();
569 let pin = self.require_pin()?;
570 let raw = self
571 .piv
572 .ecdsa_sign(&self.reader, slot, &digest, pin.as_deref())
573 .with_context(|| format!("signing with the key in slot 0x{slot:02x}"))?;
574 let signature = Signature::from_slice(&raw).context("parsing the slot signature")?;
575 let matches = VerifyingKey::from(&public_key)
576 .verify_prehash(&digest, &signature)
577 .is_ok();
578 Ok(if matches {
579 SlotKeyCheck::Match
580 } else {
581 SlotKeyCheck::Mismatch
582 })
583 }
584
585 pub fn take_flash(&mut self) -> Option<Box<dyn crate::piv::FlashHandle>> {
591 self.flash_handle.take()
592 }
593
594 pub fn get_public_key(&self, slot: u8) -> Result<PublicKey> {
596 let cert_der = self
597 .piv
598 .read_certificate(&self.reader, slot)
599 .with_context(|| format!("reading certificate from slot 0x{slot:02x}"))?;
600 parse_ec_public_key_from_cert_der(&cert_der)
601 }
602}
603
604fn probe_card(
614 reader: &str,
615 piv: &dyn PivBackend,
616) -> (auxiliaries::DefaultCredentials, ProtectionMode) {
617 let defaults = if std::env::var("YB_SKIP_DEFAULT_CHECK").is_ok() {
618 auxiliaries::DefaultCredentials::default()
619 } else {
620 auxiliaries::detect_default_credentials(reader, piv)
621 };
622 (defaults, auxiliaries::detect_protection_mode(reader, piv))
623}
624
625fn initial_pin(
628 explicit: Option<String>,
629 defaults: &auxiliaries::DefaultCredentials,
630) -> RefCell<Option<Zeroizing<String>>> {
631 let pin = explicit.or_else(|| defaults.pin.then(|| auxiliaries::DEFAULT_PIN.to_owned()));
632 RefCell::new(pin.map(Zeroizing::new))
633}
634
635#[allow(clippy::type_complexity)]
640fn select_device(
641 devices: &[DeviceInfo],
642 serial: Option<&u32>,
643 reader: Option<&str>,
644 piv: &Arc<dyn PivBackend>,
645 device_picker: &dyn Fn(
646 &Arc<dyn PivBackend>,
647 &[DeviceInfo],
648 ) -> Result<
649 Option<(DeviceInfo, Option<Box<dyn crate::piv::FlashHandle>>)>,
650 >,
651) -> Result<(DeviceInfo, String, Option<Box<dyn crate::piv::FlashHandle>>)> {
652 if let Some(s) = serial {
653 let dev = devices.iter().find(|d| &d.serial == s).ok_or_else(|| {
654 YbError::new("no connected YubiKey has the requested serial number")
656 .fix("`ykman list --serials` lists the connected YubiKeys")
657 })?;
658 return Ok((dev.clone(), dev.reader.clone(), None));
659 }
660
661 if let Some(r) = reader {
662 let dev = devices.iter().find(|d| d.reader == r).ok_or_else(|| {
663 YbError::new("no YubiKey on the requested reader")
664 .fix("`yb list-readers` lists the readers")
665 })?;
666 return Ok((dev.clone(), r.to_owned(), None));
667 }
668
669 match devices.len() {
670 0 => Err(CardError::Pcsc {
671 op: PcscOp::ListReaders,
672 code: PcscCode::NoReaders,
673 }
674 .into()),
675 1 => Ok((devices[0].clone(), devices[0].reader.clone(), None)),
676 _ => {
677 match device_picker(piv, devices)? {
679 Some((dev, flash)) => {
680 let reader = dev.reader.clone();
681 Ok((dev, reader, flash))
682 }
683 None => bail!("device selection cancelled"),
684 }
685 }
686 }
687}
688
689pub fn parse_ec_public_key_from_cert_der(cert_der: &[u8]) -> Result<PublicKey> {
695 use der::Decode;
696 use p256::elliptic_curve::sec1::FromEncodedPoint;
697 use x509_cert::Certificate;
698
699 let cert = Certificate::from_der(cert_der).context("parsing DER certificate")?;
700
701 let spki = &cert.tbs_certificate.subject_public_key_info;
703 let point_bytes = spki.subject_public_key.as_bytes().ok_or_else(|| {
704 anyhow::anyhow!("certificate SubjectPublicKeyInfo: unexpected bit-string encoding")
705 })?;
706
707 let encoded = p256::EncodedPoint::from_bytes(point_bytes)
708 .map_err(|e| anyhow::anyhow!("parsing EC point from SPKI: {e}"))?;
709 let pk: Option<p256::PublicKey> = p256::PublicKey::from_encoded_point(&encoded).into();
710 pk.ok_or_else(|| anyhow::anyhow!("EC point in certificate is not on P-256 curve"))
711}
712
713#[cfg(all(test, feature = "virtual-piv"))]
718mod tests {
719 use super::*;
720 use crate::piv::VirtualPiv;
721
722 #[test]
726 fn skip_default_check_disables_detection() {
727 let piv: Arc<dyn PivBackend> = Arc::new(VirtualPiv::new());
729 let ctx = Context::with_backend(piv.clone(), None, false).unwrap();
730 assert!(ctx.defaults.any());
731 assert_eq!(ctx.require_pin().unwrap().as_deref(), Some("123456"));
732
733 std::env::set_var("YB_SKIP_DEFAULT_CHECK", "1");
734 let ctx = Context::with_backend(piv, None, false);
735 std::env::remove_var("YB_SKIP_DEFAULT_CHECK");
736 let ctx = ctx.unwrap();
737 assert!(!ctx.defaults.any());
738 assert_eq!(ctx.require_pin().unwrap(), None, "no PIN filled in");
739 assert!(ctx
740 .enforce_default_policy(SecretOp::Store)
741 .unwrap()
742 .is_empty());
743 }
744}