Skip to main content

yb_core/
context.rs

1// SPDX-FileCopyrightText: 2025 - 2026 Frederic Ruget <fred@atlant.is> (GitHub: @douzebis)
2//
3// SPDX-License-Identifier: MIT
4
5//! Runtime context shared across all CLI commands.
6
7#[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/// Output-control flags passed to `Context::new`.
22#[derive(Debug, Clone, Copy, Default)]
23pub struct OutputOptions {
24    pub debug: bool,
25    pub quiet: bool,
26}
27
28/// Callback type for interactive device selection.
29///
30/// Returns the selected device together with an optional flash handle.  When
31/// the picker has been flashing the LED of the chosen device, it may pass that
32/// handle through so the flash continues uninterrupted into the next prompt.
33pub 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/// Plain configuration options for `Context::new`.
41#[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/// Where the management key used for writes came from (spec 0022 §1
51/// step 4).
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum KeySource {
54    /// `YB_MANAGEMENT_KEY` (or the deprecated `--key`).
55    Explicit,
56    /// PRINTED tag `89`.
57    Printed,
58    /// PRINTED tag `8A`: the previous key, kept during an interrupted switch.
59    PrintedPrevious,
60    /// The factory default, not stored anywhere.
61    FactoryDefault,
62}
63
64impl KeySource {
65    /// Whether the key was read from the PRINTED object.
66    pub fn is_printed(self) -> bool {
67        matches!(self, Self::Printed | Self::PrintedPrevious)
68    }
69}
70
71/// What to do to PRINTED after resolving the management key.
72#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
73pub enum PrintedRepair {
74    #[default]
75    None,
76    /// Rewrite as `88 { 89 <key> }`, dropping a leftover tag `8A`.
77    Rewrite,
78    /// Delete: the card uses the factory key, which needs no storage.
79    Delete,
80}
81
82/// Repairs due after resolving the management key (spec 0022 §1 step 4).
83#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
84pub struct Repairs {
85    /// ADMIN DATA does not record the stored key in the standard layout
86    /// (legacy yb flag, or an interrupted `--protect`).
87    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
97/// What the resolver reports when no candidate key is accepted: the key
98/// must be supplied (see [`Context::management_key_for_write`]).
99pub const MANAGEMENT_KEY_NOT_FOUND: &str =
100    "the management key is not in PRINTED and is not the factory default";
101
102/// Outcome of [`Context::check_slot_key`].
103#[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    /// Firmware version of the selected YubiKey, as reported by the device
130    /// list (e.g. "5.4.3").  Only the unexpected-error report uses it.
131    pub firmware: String,
132    pub management_key: Option<String>,
133    /// Which factory-default credentials were still active at startup.
134    pub defaults: auxiliaries::DefaultCredentials,
135    /// `--allow-defaults`: the default-credential policy warns instead of
136    /// refusing (spec 0024 §3).
137    pub allow_defaults: bool,
138    /// Cached PIN.  Starts as `None` when no non-interactive source provided
139    /// one; populated on the first call to `require_pin()`.
140    /// Wrapped in `Zeroizing` so the bytes are overwritten on drop.
141    pin: RefCell<Option<Zeroizing<String>>>,
142    /// Called by `require_pin()` when `pin` is still `None`.
143    /// Returns `Some(pin)` if it can supply one, `None` otherwise.
144    pin_fn: Box<dyn Fn() -> Result<Option<String>>>,
145    pub piv: Arc<dyn PivBackend>,
146    pub debug: bool,
147    pub quiet: bool,
148    /// How the management key is protected, from ADMIN DATA at startup.
149    pub protection: ProtectionMode,
150    /// The management key for writes, once resolved (see
151    /// [`Context::management_key_for_write`]).
152    resolved: RefCell<Option<ResolvedKey>>,
153    /// Optional flash handle passed in from the interactive device picker.
154    /// Kept alive so the LED continues to flash into the next prompt.
155    /// Consumed by [`Context::take_flash`].
156    pub flash_handle: Option<Box<dyn crate::piv::FlashHandle>>,
157}
158
159impl Context {
160    /// Build a Context from global CLI options, selecting the device.
161    ///
162    /// `pin` is the PIN resolved from non-interactive sources (env var, stdin,
163    /// deprecated flag).  `pin_fn` is called by `require_pin()` the first time
164    /// a PIN is needed and `pin` is still `None` — typically a TTY prompt
165    /// closure supplied by the application layer.
166    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    /// Build a `Context` from an explicit PIV backend.
214    ///
215    /// Use this when you have a `VirtualPiv` (for tests) or any other
216    /// custom `PivBackend` implementation.  The backend must expose exactly
217    /// one device; if it exposes none or more than one, an error is returned.
218    ///
219    /// The card is probed exactly as by [`Context::new`] (default
220    /// credentials, protection mode).  `allow_defaults` starts false.
221    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    /// Return the PIN, invoking `pin_fn` on first call if not yet resolved.
256    ///
257    /// Resolution order:
258    /// 1. Already-cached PIN (from a non-interactive source or a prior call).
259    /// 2. `pin_fn()` — supplied by the caller of `Context::new`; typically a
260    ///    TTY prompt closure in the application layer.  The result is cached so
261    ///    subsequent calls never invoke `pin_fn` again.
262    /// 3. `None` — the caller must decide whether to error.
263    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    /// The PIN known so far (from a non-interactive source, the factory
273    /// default, or an earlier prompt), without prompting.
274    pub fn known_pin(&self) -> Option<String> {
275        self.pin.borrow().as_ref().map(|z| z.as_str().to_owned())
276    }
277
278    /// Replace the cached PIN, e.g. after changing it on the card.
279    pub fn set_pin(&self, pin: &str) {
280        *self.pin.borrow_mut() = Some(Zeroizing::new(pin.to_owned()));
281    }
282
283    /// Detect the factory-default credentials again, after changing some
284    /// of them (spec 0023 §6).
285    pub fn refresh_defaults(&mut self) {
286        self.defaults = probe_card(&self.reader, self.piv.as_ref()).0;
287    }
288
289    /// Apply the default-credential policy for `op` (spec 0024): refuse
290    /// with `Err`, or return the warnings that apply, printed unless
291    /// `--quiet`.  Call once per command, before its first card write.
292    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    /// Fail if the card's management key protection is one yb cannot write
303    /// with (PIN-derived, or unparseable ADMIN DATA).
304    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    /// Return the management key to use for write operations, after
320    /// checking that the card accepts it (spec 0022 §1 step 4).
321    ///
322    /// This is the single place in yb that reads PRINTED for a key.  The
323    /// first candidate the card accepts wins:
324    /// 1. the explicit key (`YB_MANAGEMENT_KEY`) — if rejected, fail;
325    /// 2. PRINTED tag `89`, then tag `8A` (kept during a key switch), when
326    ///    a PIN is available — whatever the ADMIN DATA flags say;
327    /// 3. the factory default.
328    ///
329    /// A key found in PRINTED may leave repairs due (see [`Repairs`]),
330    /// carried out by [`Context::complete_pending_repairs`].  The result
331    /// is cached for the rest of the invocation.
332    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                // Verify first, so that a wrong PIN is reported as such and
360                // not mistaken for an empty PRINTED object.
361                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                // The factory key needs no protection: drop any key left in
393                // PRINTED (e.g. a switch from the factory key that did not
394                // take effect).
395                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    /// Whether the management key is already PIN-protected (spec 0023
421    /// §3a): PRINTED tag `89` holds the key the card accepts, however that
422    /// key was obtained.  The factory key never counts as protected.
423    ///
424    /// Resolves the management key first.  When the key was given
425    /// explicitly, PRINTED is read with the PIN, and the repairs a
426    /// protected card needs (legacy flag, leftover tag `8A`) are recorded
427    /// as for a key read from PRINTED.
428    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    /// Whether PRINTED holds the resolved management key, in tag `89` or
465    /// `8A`, whatever the key's source (spec 0027 §3a).  A failed key
466    /// switch puts it back there.
467    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    /// Where the resolved management key came from, once resolved.
488    pub fn management_key_source(&self) -> Option<KeySource> {
489        self.resolved.borrow().as_ref().map(|r| r.source)
490    }
491
492    /// Repairs due from the management key resolution (none before it).
493    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    /// Carry out the repairs found by [`Context::management_key_for_write`]:
502    /// rewrite ADMIN DATA in the standard layout (spec 0021 §4) and/or
503    /// clean up PRINTED (spec 0022 §1 step 4).
504    ///
505    /// Call once the command's own writes have succeeded.  A failure is a
506    /// warning and does not fail the command: the next write retries.
507    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    /// Check that the key in `slot` matches the public key in the slot's
551    /// certificate, by signing a random digest and verifying the signature
552    /// (spec 0022 §1 step 5).  Needs the PIN.
553    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    /// Take the flash handle passed in from the interactive device picker.
580    ///
581    /// Returns `Some(handle)` the first time it is called (if the picker
582    /// provided one), and `None` on all subsequent calls.  The LED stops
583    /// flashing when the returned handle is dropped.
584    pub fn take_flash(&mut self) -> Option<Box<dyn crate::piv::FlashHandle>> {
585        self.flash_handle.take()
586    }
587
588    /// Retrieve the YubiKey's public key from the given PIV slot.
589    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
598// ---------------------------------------------------------------------------
599// Card probe shared by both constructors
600// ---------------------------------------------------------------------------
601
602/// Read-only state probed when a `Context` is built: which credentials are
603/// still at their factory values (unless `YB_SKIP_DEFAULT_CHECK` is set),
604/// and how the management key is protected.  Nothing is enforced here:
605/// unsupported protection modes are refused only when a management key is
606/// needed, and default credentials per command (spec 0024).
607fn 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
619/// The PIN a `Context` starts with: an explicit one always wins; otherwise
620/// the factory PIN when the card reports it (spec 0024 §2a).
621fn 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// ---------------------------------------------------------------------------
630// Device selection
631// ---------------------------------------------------------------------------
632
633#[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            // Not echoing the serial: it would end up in pasted reports.
649            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            // Multiple devices: invoke the picker (interactive or fallback).
672            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
683// ---------------------------------------------------------------------------
684// Certificate / public-key parsing
685// ---------------------------------------------------------------------------
686
687/// Extract the EC P-256 public key from a DER-encoded X.509 certificate.
688pub 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    // SubjectPublicKeyInfo → raw bit string → uncompressed EC point.
696    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// ---------------------------------------------------------------------------
708// Tests
709// ---------------------------------------------------------------------------
710
711#[cfg(all(test, feature = "virtual-piv"))]
712mod tests {
713    use super::*;
714
715    /// `YB_SKIP_DEFAULT_CHECK` disables detection in both constructors
716    /// (spec 0024 §3).  This is the only test in this binary that builds a
717    /// `Context`, so setting the process-wide variable cannot race.
718    #[test]
719    fn skip_default_check_disables_detection() {
720        // VirtualPiv::new() keeps every factory credential.
721        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}