1#[cfg(feature = "virtual-piv")]
9use crate::piv::VirtualPiv;
10use crate::{
11 auxiliaries,
12 piv::{hardware::HardwarePiv, DeviceInfo, PivBackend},
13};
14use anyhow::{bail, Context as _, Result};
15use p256::PublicKey;
16use std::cell::RefCell;
17use std::sync::Arc;
18use zeroize::Zeroizing;
19
20#[derive(Debug, Clone, Copy, Default)]
22pub struct OutputOptions {
23 pub debug: bool,
24 pub quiet: bool,
25}
26
27pub type DevicePicker = Box<
33 dyn Fn(
34 &Arc<dyn PivBackend>,
35 &[DeviceInfo],
36 ) -> Result<Option<(DeviceInfo, Option<Box<dyn crate::piv::FlashHandle>>)>>,
37>;
38
39#[derive(Debug, Default)]
41pub struct ContextOptions {
42 pub serial: Option<u32>,
43 pub reader: Option<String>,
44 pub management_key: Option<String>,
45 pub pin: Option<String>,
46 pub allow_defaults: bool,
47}
48
49pub struct Context {
50 pub reader: String,
51 pub serial: u32,
52 pub management_key: Option<String>,
53 pub defaults: auxiliaries::DefaultCredentials,
55 pin: RefCell<Option<Zeroizing<String>>>,
59 pin_fn: Box<dyn Fn() -> Result<Option<String>>>,
62 pub piv: Arc<dyn PivBackend>,
63 pub debug: bool,
64 pub quiet: bool,
65 pub pin_protected: bool,
66 pub flash_handle: Option<Box<dyn crate::piv::FlashHandle>>,
70}
71
72impl Context {
73 pub fn new(
80 opts: ContextOptions,
81 pin_fn: Box<dyn Fn() -> Result<Option<String>>>,
82 device_picker: DevicePicker,
83 output: OutputOptions,
84 ) -> Result<Self> {
85 let debug = output.debug;
86 let quiet = output.quiet;
87 #[cfg(feature = "virtual-piv")]
88 let piv: Arc<dyn PivBackend> = if let Ok(path) = std::env::var("YB_FIXTURE") {
89 Arc::new(VirtualPiv::from_fixture(std::path::Path::new(&path))?)
90 } else {
91 Arc::new(HardwarePiv::new())
92 };
93 #[cfg(not(feature = "virtual-piv"))]
94 let piv: Arc<dyn PivBackend> = Arc::new(HardwarePiv::new());
95
96 let devices = piv.list_devices().context("listing YubiKey devices")?;
97
98 let (device, selected_reader, flash_handle) = select_device(
99 &devices,
100 opts.serial.as_ref(),
101 opts.reader.as_deref(),
102 &piv,
103 &*device_picker,
104 )?;
105
106 let defaults = if std::env::var("YB_SKIP_DEFAULT_CHECK").is_err() {
108 auxiliaries::check_for_default_credentials(
109 &selected_reader,
110 piv.as_ref(),
111 opts.allow_defaults,
112 )?
113 } else {
114 auxiliaries::DefaultCredentials::default()
115 };
116
117 let management_key = opts.management_key.or_else(|| {
120 if defaults.management_key {
121 Some(auxiliaries::DEFAULT_MANAGEMENT_KEY.to_owned())
122 } else {
123 None
124 }
125 });
126 let pin = opts.pin.or_else(|| {
127 if defaults.pin {
128 Some(auxiliaries::DEFAULT_PIN.to_owned())
129 } else {
130 None
131 }
132 });
133
134 let (pin_protected, pin_derived) =
135 auxiliaries::detect_pin_protected_mode(&selected_reader, piv.as_ref())
136 .unwrap_or((false, false));
137
138 reject_pin_derived(pin_derived)?;
139
140 Ok(Self {
141 reader: selected_reader,
142 serial: device.serial,
143 management_key,
144 defaults,
145 pin: RefCell::new(pin.map(Zeroizing::new)),
146 pin_fn,
147 piv,
148 debug,
149 quiet,
150 pin_protected,
151 flash_handle,
152 })
153 }
154
155 pub fn with_backend(
164 backend: Arc<dyn PivBackend>,
165 pin: Option<String>,
166 debug: bool,
167 ) -> Result<Self> {
168 let devices = backend
169 .list_devices()
170 .context("listing devices in backend")?;
171 let device = match devices.as_slice() {
172 [] => bail!("no device found in backend"),
173 [d] => d.clone(),
174 _ => bail!("multiple devices in backend — use Context::new with --serial"),
175 };
176 let reader = device.reader.clone();
177
178 let (pin_protected, pin_derived) =
179 auxiliaries::detect_pin_protected_mode(&reader, backend.as_ref())
180 .unwrap_or((false, false));
181
182 reject_pin_derived(pin_derived)?;
183
184 Ok(Self {
185 reader,
186 serial: device.serial,
187 management_key: None,
188 defaults: auxiliaries::DefaultCredentials::default(),
189 pin: RefCell::new(pin.map(Zeroizing::new)),
190 pin_fn: Box::new(|| Ok(None)),
191 piv: backend,
192 debug,
193 quiet: false,
194 pin_protected,
195 flash_handle: None,
196 })
197 }
198
199 pub fn require_pin(&self) -> Result<Option<String>> {
208 if self.pin.borrow().is_some() {
209 return Ok(self.pin.borrow().as_ref().map(|z| z.as_str().to_owned()));
210 }
211 let resolved = (self.pin_fn)()?;
212 *self.pin.borrow_mut() = resolved.as_deref().map(|s| Zeroizing::new(s.to_owned()));
213 Ok(resolved)
214 }
215
216 pub fn management_key_for_write(&self) -> Result<Option<String>> {
220 if let Some(ref k) = self.management_key {
221 return Ok(Some(k.clone()));
222 }
223 if self.pin_protected {
224 let pin = self.require_pin()?.ok_or_else(|| {
225 anyhow::anyhow!("PIN required to retrieve PIN-protected management key")
226 })?;
227 let key = auxiliaries::get_pin_protected_management_key(
228 &self.reader,
229 self.piv.as_ref(),
230 &pin,
231 )?;
232 return Ok(Some(key));
233 }
234 Ok(None)
235 }
236
237 pub fn take_flash(&mut self) -> Option<Box<dyn crate::piv::FlashHandle>> {
243 self.flash_handle.take()
244 }
245
246 pub fn get_public_key(&self, slot: u8) -> Result<PublicKey> {
248 let cert_der = self
249 .piv
250 .read_certificate(&self.reader, slot)
251 .with_context(|| format!("reading certificate from slot 0x{slot:02x}"))?;
252 parse_ec_public_key_from_cert_der(&cert_der)
253 }
254}
255
256fn reject_pin_derived(pin_derived: bool) -> Result<()> {
257 if pin_derived {
258 bail!(
259 "PIN-derived management key mode is deprecated and not supported. \
260 Please migrate to PIN-protected mode."
261 );
262 }
263 Ok(())
264}
265
266#[allow(clippy::type_complexity)]
271fn select_device(
272 devices: &[DeviceInfo],
273 serial: Option<&u32>,
274 reader: Option<&str>,
275 piv: &Arc<dyn PivBackend>,
276 device_picker: &dyn Fn(
277 &Arc<dyn PivBackend>,
278 &[DeviceInfo],
279 ) -> Result<
280 Option<(DeviceInfo, Option<Box<dyn crate::piv::FlashHandle>>)>,
281 >,
282) -> Result<(DeviceInfo, String, Option<Box<dyn crate::piv::FlashHandle>>)> {
283 if let Some(s) = serial {
284 let dev = devices
285 .iter()
286 .find(|d| &d.serial == s)
287 .ok_or_else(|| anyhow::anyhow!("no YubiKey with serial {s} found"))?;
288 return Ok((dev.clone(), dev.reader.clone(), None));
289 }
290
291 if let Some(r) = reader {
292 let dev = devices
293 .iter()
294 .find(|d| d.reader == r)
295 .ok_or_else(|| anyhow::anyhow!("no device on reader '{r}'"))?;
296 return Ok((dev.clone(), r.to_owned(), None));
297 }
298
299 match devices.len() {
300 0 => bail!("no YubiKey found"),
301 1 => Ok((devices[0].clone(), devices[0].reader.clone(), None)),
302 _ => {
303 match device_picker(piv, devices)? {
305 Some((dev, flash)) => {
306 let reader = dev.reader.clone();
307 Ok((dev, reader, flash))
308 }
309 None => bail!("device selection cancelled"),
310 }
311 }
312 }
313}
314
315pub fn parse_ec_public_key_from_cert_der(cert_der: &[u8]) -> Result<PublicKey> {
321 use der::Decode;
322 use p256::elliptic_curve::sec1::FromEncodedPoint;
323 use x509_cert::Certificate;
324
325 let cert = Certificate::from_der(cert_der).context("parsing DER certificate")?;
326
327 let spki = &cert.tbs_certificate.subject_public_key_info;
329 let point_bytes = spki.subject_public_key.as_bytes().ok_or_else(|| {
330 anyhow::anyhow!("certificate SubjectPublicKeyInfo: unexpected bit-string encoding")
331 })?;
332
333 let encoded = p256::EncodedPoint::from_bytes(point_bytes)
334 .map_err(|e| anyhow::anyhow!("parsing EC point from SPKI: {e}"))?;
335 let pk: Option<p256::PublicKey> = p256::PublicKey::from_encoded_point(&encoded).into();
336 pk.ok_or_else(|| anyhow::anyhow!("EC point in certificate is not on P-256 curve"))
337}