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
7use 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/// Output-control flags passed to `Context::new`.
20#[derive(Debug, Clone, Copy, Default)]
21pub struct OutputOptions {
22    pub debug: bool,
23    pub quiet: bool,
24}
25
26/// Callback type for interactive device selection.
27///
28/// Returns the selected device together with an optional flash handle.  When
29/// the picker has been flashing the LED of the chosen device, it may pass that
30/// handle through so the flash continues uninterrupted into the next prompt.
31pub 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/// Plain configuration options for `Context::new`.
39#[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/// Where the management key used for writes came from (spec 0022 §1
49/// step 4).
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum KeySource {
52    /// `YB_MANAGEMENT_KEY` (or the deprecated `--key`).
53    Explicit,
54    /// PRINTED tag `89`.
55    Printed,
56    /// PRINTED tag `8A`: the previous key, kept during an interrupted switch.
57    PrintedPrevious,
58    /// The factory default, not stored anywhere.
59    FactoryDefault,
60}
61
62impl KeySource {
63    /// Whether the key was read from the PRINTED object.
64    pub fn is_printed(self) -> bool {
65        matches!(self, Self::Printed | Self::PrintedPrevious)
66    }
67}
68
69/// What to do to PRINTED after resolving the management key.
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub enum PrintedRepair {
72    #[default]
73    None,
74    /// Rewrite as `88 { 89 <key> }`, dropping a leftover tag `8A`.
75    Rewrite,
76    /// Delete: the card uses the factory key, which needs no storage.
77    Delete,
78}
79
80/// Repairs due after resolving the management key (spec 0022 §1 step 4).
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
82pub struct Repairs {
83    /// ADMIN DATA does not record the stored key in the standard layout
84    /// (legacy yb flag, or an interrupted `--protect`).
85    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
95/// What the resolver reports when no candidate key is accepted: the key
96/// must be supplied (see [`Context::management_key_for_write`]).
97pub const MANAGEMENT_KEY_NOT_FOUND: &str =
98    "the management key is not in PRINTED and is not the factory default";
99
100/// Outcome of [`Context::check_slot_key`].
101#[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    /// Firmware version of the selected YubiKey, as reported by the device
128    /// list (e.g. "5.4.3").  Only the unexpected-error report uses it.
129    pub firmware: String,
130    /// Held in zeroizing memory (spec 0034 G1).
131    pub management_key: Option<Zeroizing<String>>,
132    /// Which factory-default credentials were still active at startup.
133    pub defaults: auxiliaries::DefaultCredentials,
134    /// `--allow-defaults`: the default-credential policy warns instead of
135    /// refusing (spec 0024 §3).
136    pub allow_defaults: bool,
137    /// Cached PIN.  Starts as `None` when no non-interactive source provided
138    /// one; populated on the first call to `require_pin()`.
139    /// Wrapped in `Zeroizing` so the bytes are overwritten on drop.
140    pin: RefCell<Option<Zeroizing<String>>>,
141    /// Called by `require_pin()` when `pin` is still `None`.
142    /// Returns `Some(pin)` if it can supply one, `None` otherwise.
143    pub pin_fn: Box<dyn Fn() -> Result<Option<String>>>,
144    pub piv: Arc<dyn PivBackend>,
145    pub debug: bool,
146    pub quiet: bool,
147    /// How the management key is protected, from ADMIN DATA at startup.
148    pub protection: ProtectionMode,
149    /// The management key for writes, once resolved (see
150    /// [`Context::management_key_for_write`]).
151    resolved: RefCell<Option<ResolvedKey>>,
152    /// Optional flash handle passed in from the interactive device picker.
153    /// Kept alive so the LED continues to flash into the next prompt.
154    /// Consumed by [`Context::take_flash`].
155    pub flash_handle: Option<Box<dyn crate::piv::FlashHandle>>,
156}
157
158impl Context {
159    /// Build a Context from global CLI options, selecting the device.
160    ///
161    /// `pin` is the PIN resolved from non-interactive sources (env var, stdin,
162    /// deprecated flag).  `pin_fn` is called by `require_pin()` the first time
163    /// a PIN is needed and `pin` is still `None` — typically a TTY prompt
164    /// closure supplied by the application layer.
165    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    /// Build a `Context` from an explicit PIV backend.
206    ///
207    /// Use this when you have a `VirtualPiv` (for tests) or any other
208    /// custom `PivBackend` implementation.  The backend must expose exactly
209    /// one device; if it exposes none or more than one, an error is returned.
210    ///
211    /// The card is probed exactly as by [`Context::new`] (default
212    /// credentials, protection mode).  `allow_defaults` starts false.
213    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    /// Return the PIN, invoking `pin_fn` on first call if not yet resolved.
248    ///
249    /// Resolution order:
250    /// 1. Already-cached PIN (from a non-interactive source or a prior call).
251    /// 2. `pin_fn()` — supplied by the caller of `Context::new`; typically a
252    ///    TTY prompt closure in the application layer.  The result is cached so
253    ///    subsequent calls never invoke `pin_fn` again.
254    /// 3. `None` — the caller must decide whether to error.
255    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    /// The PIN known so far (from a non-interactive source, the factory
265    /// default, or an earlier prompt), without prompting.
266    pub fn known_pin(&self) -> Option<String> {
267        self.pin.borrow().as_ref().map(|z| z.as_str().to_owned())
268    }
269
270    /// Replace the cached PIN, e.g. after changing it on the card.
271    pub fn set_pin(&self, pin: &str) {
272        *self.pin.borrow_mut() = Some(Zeroizing::new(pin.to_owned()));
273    }
274
275    /// Forget the PIN and the resolved management key, overwriting them in
276    /// memory.  Library sessions hold secrets for one call only (spec 0039
277    /// G5).
278    pub fn clear_secrets(&self) {
279        *self.pin.borrow_mut() = None;
280        *self.resolved.borrow_mut() = None;
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<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                // 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            (
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                // The factory key needs no protection: drop any key left in
399                // PRINTED (e.g. a switch from the factory key that did not
400                // take effect).
401                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    /// Whether the management key is already PIN-protected (spec 0023
427    /// §3a): PRINTED tag `89` holds the key the card accepts, however that
428    /// key was obtained.  The factory key never counts as protected.
429    ///
430    /// Resolves the management key first.  When the key was given
431    /// explicitly, PRINTED is read with the PIN, and the repairs a
432    /// protected card needs (legacy flag, leftover tag `8A`) are recorded
433    /// as for a key read from PRINTED.
434    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    /// Whether PRINTED holds the resolved management key, in tag `89` or
471    /// `8A`, whatever the key's source (spec 0027 §3a).  A failed key
472    /// switch puts it back there.
473    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    /// Where the resolved management key came from, once resolved.
494    pub fn management_key_source(&self) -> Option<KeySource> {
495        self.resolved.borrow().as_ref().map(|r| r.source)
496    }
497
498    /// Repairs due from the management key resolution (none before it).
499    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    /// Carry out the repairs found by [`Context::management_key_for_write`]:
508    /// rewrite ADMIN DATA in the standard layout (spec 0021 §4) and/or
509    /// clean up PRINTED (spec 0022 §1 step 4).
510    ///
511    /// Call once the command's own writes have succeeded.  A failure is a
512    /// warning and does not fail the command: the next write retries.
513    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    /// Check that the key in `slot` matches the public key in the slot's
557    /// certificate, by signing a random digest and verifying the signature
558    /// (spec 0022 §1 step 5).  Needs the PIN.
559    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    /// Take the flash handle passed in from the interactive device picker.
586    ///
587    /// Returns `Some(handle)` the first time it is called (if the picker
588    /// provided one), and `None` on all subsequent calls.  The LED stops
589    /// flashing when the returned handle is dropped.
590    pub fn take_flash(&mut self) -> Option<Box<dyn crate::piv::FlashHandle>> {
591        self.flash_handle.take()
592    }
593
594    /// Retrieve the YubiKey's public key from the given PIV slot.
595    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
604// ---------------------------------------------------------------------------
605// Card probe shared by both constructors
606// ---------------------------------------------------------------------------
607
608/// Read-only state probed when a `Context` is built: which credentials are
609/// still at their factory values (unless `YB_SKIP_DEFAULT_CHECK` is set),
610/// and how the management key is protected.  Nothing is enforced here:
611/// unsupported protection modes are refused only when a management key is
612/// needed, and default credentials per command (spec 0024).
613fn 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
625/// The PIN a `Context` starts with: an explicit one always wins; otherwise
626/// the factory PIN when the card reports it (spec 0024 §2a).
627fn 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// ---------------------------------------------------------------------------
636// Device selection
637// ---------------------------------------------------------------------------
638
639#[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            // Not echoing the serial: it would end up in pasted reports.
655            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            // Multiple devices: invoke the picker (interactive or fallback).
678            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
689// ---------------------------------------------------------------------------
690// Certificate / public-key parsing
691// ---------------------------------------------------------------------------
692
693/// Extract the EC P-256 public key from a DER-encoded X.509 certificate.
694pub 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    // SubjectPublicKeyInfo → raw bit string → uncompressed EC point.
702    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// ---------------------------------------------------------------------------
714// Tests
715// ---------------------------------------------------------------------------
716
717#[cfg(all(test, feature = "virtual-piv"))]
718mod tests {
719    use super::*;
720    use crate::piv::VirtualPiv;
721
722    /// `YB_SKIP_DEFAULT_CHECK` disables detection in both constructors
723    /// (spec 0024 §3).  This is the only test in this binary that builds a
724    /// `Context`, so setting the process-wide variable cannot race.
725    #[test]
726    fn skip_default_check_disables_detection() {
727        // VirtualPiv::new() keeps every factory credential.
728        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}