1use crate::auxiliaries::ProtectionMode;
13use crate::context::{Context, SlotKeyCheck};
14use crate::piv::mgmt::{
15 get_metadata_apdu, key_algorithm_name, parse_credential_metadata, parse_mgmt_metadata,
16 parse_slot_metadata, CredentialMetadata, KeyOrigin, MgmtMetadata, SlotMetadata, TouchPolicy,
17 ALGO_ECCP256, GET_METADATA_MGMT, GET_METADATA_PIN, GET_METADATA_PUK,
18};
19use crate::piv::PivBackend;
20use crate::store::{constants::OBJECT_ID_ZERO, Store};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24pub enum Severity {
25 Ok,
26 Warning,
27 Error,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum SlotCertificate {
33 None,
34 P256 {
36 subject: String,
37 },
38 NotP256 {
40 subject: String,
41 },
42 Unparseable,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct SlotReport {
49 pub slot: u8,
50 pub key: Option<SlotMetadata>,
52 pub certificate: SlotCertificate,
53}
54
55impl SlotReport {
56 pub fn is_occupied(&self) -> bool {
58 self.key.is_some() || self.certificate != SlotCertificate::None
59 }
60
61 pub fn certificate_unusable(&self) -> bool {
63 matches!(
64 self.certificate,
65 SlotCertificate::NotP256 { .. } | SlotCertificate::Unparseable
66 )
67 }
68}
69
70pub enum StorePresence {
72 None,
73 Present(Store),
74 Unreadable(String),
76}
77
78impl StorePresence {
79 pub fn probe(reader: &str, piv: &dyn PivBackend) -> Self {
81 match Store::from_device(reader, piv) {
82 Ok(store) => Self::Present(store),
83 Err(e) => match piv.object_size(reader, OBJECT_ID_ZERO) {
84 Ok(Some(_)) => Self::Unreadable(format!("{e:#}")),
85 _ => Self::None,
86 },
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Item {
94 pub label: String,
95 pub text: String,
96 pub severity: Severity,
97 pub hint: Vec<String>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct CardReport {
104 pub serial: u32,
105 pub firmware: String,
106 pub pin: Option<CredentialMetadata>,
108 pub puk: Option<CredentialMetadata>,
109 pub management_key: Option<MgmtMetadata>,
110 pub protection: ProtectionMode,
111 pub slot: SlotReport,
112 pub key_check: Option<SlotKeyCheck>,
114}
115
116impl CardReport {
117 pub fn build(ctx: &Context, slot: u8) -> Self {
120 let (reader, piv) = (ctx.reader.as_str(), ctx.piv.as_ref());
121 let metadata = |apdu: &[u8]| piv.send_apdu(reader, apdu).ok();
122 Self {
123 serial: ctx.serial,
124 firmware: ctx.firmware.clone(),
125 pin: metadata(&GET_METADATA_PIN).and_then(|d| parse_credential_metadata(&d).ok()),
126 puk: metadata(&GET_METADATA_PUK).and_then(|d| parse_credential_metadata(&d).ok()),
127 management_key: metadata(&GET_METADATA_MGMT).and_then(|d| parse_mgmt_metadata(&d).ok()),
128 protection: ctx.protection,
129 slot: SlotReport {
130 slot,
131 key: metadata(&get_metadata_apdu(slot)).and_then(|d| parse_slot_metadata(&d).ok()),
132 certificate: read_slot_certificate(reader, piv, slot),
133 },
134 key_check: None,
135 }
136 }
137
138 pub fn has_metadata(&self) -> bool {
140 self.pin.is_some()
141 }
142
143 pub fn pin_blocked(&self) -> bool {
144 self.pin.is_some_and(|m| m.retries_left == 0)
145 }
146
147 pub fn puk_blocked(&self) -> bool {
148 self.puk.is_some_and(|m| m.retries_left == 0)
149 }
150
151 pub fn items(&self, key_check_hint: Option<&str>) -> Vec<Item> {
155 let mut items = vec![
156 credential_item("PIN", self.pin),
157 credential_item("PUK", self.puk),
158 self.management_key_item(),
159 self.slot_item(),
160 ];
161 let check = match self.key_check {
162 Some(SlotKeyCheck::Match) => Some(("match".to_owned(), Severity::Ok)),
163 Some(SlotKeyCheck::Mismatch) => Some((
164 "the key does not match its certificate".to_owned(),
165 Severity::Error,
166 )),
167 Some(SlotKeyCheck::NoCertificate) => Some(("no certificate".to_owned(), Severity::Ok)),
168 None => key_check_hint.map(|hint| (format!("not checked ({hint})"), Severity::Ok)),
169 };
170 if let Some((text, severity)) = check {
171 items.push(item("Key/certificate", text, severity));
172 }
173 items
174 }
175
176 pub fn severity(&self) -> Severity {
178 self.items(None)
179 .iter()
180 .map(|i| i.severity)
181 .max()
182 .unwrap_or(Severity::Ok)
183 }
184
185 pub fn render(&self, key_check_hint: Option<&str>, hints: bool) -> String {
188 let mut out = format!("YubiKey {} — firmware {}\n", self.serial, self.firmware);
189 for i in self.items(key_check_hint) {
190 let text = match i.severity {
191 Severity::Ok => i.text,
192 Severity::Warning => format!("warning: {}", i.text),
193 Severity::Error => format!("ERROR: {}", i.text),
194 };
195 out.push_str(&format!(" {:<16} {text}\n", i.label));
196 if hints {
197 for line in &i.hint {
198 out.push_str(&format!(" {:<16} {line}\n", ""));
199 }
200 }
201 }
202 out
203 }
204
205 fn management_key_item(&self) -> Item {
206 const LABEL: &str = "Management key";
207 let algo = match self.management_key {
208 Some(md) => md.algo.to_string(),
209 None => "unknown algorithm (firmware < 5.3)".to_owned(),
210 };
211 let (protection, mut severity) = match self.protection {
212 ProtectionMode::Standard => ("kept on the YubiKey, unlocked by the PIN", Severity::Ok),
213 ProtectionMode::LegacyOrPukBlocked => (
214 "ADMIN DATA flag 0x01 (key kept on the YubiKey by yb ≤ 0.4.x, or blocked PUK)",
215 Severity::Warning,
216 ),
217 ProtectionMode::None if self.management_key.is_some_and(|md| md.is_default) => {
219 ("", Severity::Ok)
220 }
221 ProtectionMode::None => ("not stored on the YubiKey", Severity::Ok),
222 ProtectionMode::Derived => {
223 return item(
224 LABEL,
225 format!("{algo}, PIN-derived (deprecated; yb does not support it)"),
226 Severity::Error,
227 )
228 }
229 ProtectionMode::Invalid => {
230 return item(
231 LABEL,
232 format!("{algo}, ADMIN DATA (0x5FFF00) cannot be parsed"),
233 Severity::Error,
234 )
235 }
236 };
237 let mut parts = vec![algo];
238 if !protection.is_empty() {
239 parts.push(protection.to_owned());
240 }
241 if let Some(md) = self.management_key {
242 if md.is_default {
243 parts.push("factory default".to_owned());
244 severity = severity.max(Severity::Warning);
245 }
246 match md.touch {
247 TouchPolicy::Always => {
248 parts.push("touch always required".to_owned());
249 severity = severity.max(Severity::Warning);
250 }
251 TouchPolicy::Cached => parts.push("touch required (cached)".to_owned()),
252 TouchPolicy::Never => {}
253 }
254 }
255 let mut item = item(LABEL, parts.join(", "), severity);
256 if self.protection == ProtectionMode::None
259 && self.management_key.is_some_and(|md| !md.is_default)
260 {
261 item.hint = vec![
262 "yb store and yb remove need it each time (YB_MANAGEMENT_KEY).".to_owned(),
263 "To have yb keep it on the YubiKey, unlocked by your PIN:".to_owned(),
264 "yb rotate-management-key".to_owned(),
265 ];
266 }
267 item
268 }
269
270 fn slot_item(&self) -> Item {
271 let label = format!("Slot 0x{:02x}", self.slot.slot);
272 let mut parts = Vec::new();
273 if let Some(key) = self.slot.key {
274 parts.push(key_algorithm_name(key.algorithm));
275 match key.origin {
276 Some(KeyOrigin::Generated) => parts.push("generated on card".to_owned()),
277 Some(KeyOrigin::Imported) => parts.push("imported".to_owned()),
278 None => {}
279 }
280 }
281 let mut severity = Severity::Ok;
282 match &self.slot.certificate {
283 SlotCertificate::None if self.slot.key.is_some() => {
284 parts.push("no certificate".to_owned())
285 }
286 SlotCertificate::None => parts.push("empty".to_owned()),
287 SlotCertificate::P256 { subject } => parts.push(format!("certificate {subject}")),
288 SlotCertificate::NotP256 { subject } => {
289 parts.push(format!(
290 "certificate {subject} does not hold an EC P-256 key"
291 ));
292 severity = Severity::Error;
293 }
294 SlotCertificate::Unparseable => {
295 parts.push("the certificate cannot be parsed".to_owned());
296 severity = Severity::Error;
297 }
298 }
299 if self.slot.key.is_some_and(|k| k.algorithm != ALGO_ECCP256) {
300 severity = Severity::Error;
301 }
302 item(&label, parts.join(", "), severity)
303 }
304}
305
306fn item(label: &str, text: String, severity: Severity) -> Item {
307 Item {
308 label: label.to_owned(),
309 text,
310 severity,
311 hint: Vec::new(),
312 }
313}
314
315fn credential_item(label: &str, md: Option<CredentialMetadata>) -> Item {
316 let Some(md) = md else {
317 return item(label, "unknown (firmware < 5.3)".to_owned(), Severity::Ok);
318 };
319 let tries = format!("{}/{} tries left", md.retries_left, md.retries_total);
320 if md.retries_left == 0 {
321 let severity = if label == "PIN" {
323 Severity::Error
324 } else {
325 Severity::Warning
326 };
327 return item(label, format!("blocked ({tries})"), severity);
328 }
329 if md.is_default {
330 return item(
331 label,
332 format!("factory default ({tries})"),
333 Severity::Warning,
334 );
335 }
336 item(label, format!("ok ({tries})"), Severity::Ok)
337}
338
339fn read_slot_certificate(reader: &str, piv: &dyn PivBackend, slot: u8) -> SlotCertificate {
340 use der::Decode;
341 use x509_cert::Certificate;
342
343 let Ok(der) = piv.read_certificate(reader, slot) else {
344 return SlotCertificate::None;
345 };
346 let Ok(cert) = Certificate::from_der(&der) else {
347 return SlotCertificate::Unparseable;
348 };
349 let subject = cert.tbs_certificate.subject.to_string();
350 if crate::context::parse_ec_public_key_from_cert_der(&der).is_ok() {
351 SlotCertificate::P256 { subject }
352 } else {
353 SlotCertificate::NotP256 { subject }
354 }
355}
356
357#[cfg(all(test, feature = "virtual-piv"))]
362mod tests {
363 use super::*;
364 use crate::piv::VirtualPiv;
365 use std::sync::Arc;
366
367 const MGMT: &str = crate::auxiliaries::DEFAULT_MANAGEMENT_KEY;
368
369 #[test]
370 fn factory_card() {
371 let piv: Arc<dyn PivBackend> = Arc::new(VirtualPiv::new());
372 let ctx = Context::with_backend(piv, None, false).unwrap();
373 let report = CardReport::build(&ctx, 0x82);
374 assert!(report.has_metadata());
375 let text = report.render(Some("use --check-key"), true);
376 assert_eq!(
377 text,
378 "YubiKey 99999999 — firmware 5.4.3\n\
379 \x20 PIN warning: factory default (3/3 tries left)\n\
380 \x20 PUK warning: factory default (3/3 tries left)\n\
381 \x20 Management key warning: 3DES, factory default\n\
382 \x20 Slot 0x82 empty\n\
383 \x20 Key/certificate not checked (use --check-key)\n"
384 );
385 assert_eq!(report.severity(), Severity::Warning);
386 assert!(!report.slot.is_occupied());
387 }
388
389 #[test]
390 fn generated_key_with_certificate() {
391 let piv: Arc<dyn PivBackend> = Arc::new(VirtualPiv::new());
392 let reader = piv.list_devices().unwrap()[0].reader.clone();
393 piv.generate_certificate(&reader, 0x82, "/CN=YBLOB ECCP256", MGMT, None)
394 .unwrap();
395 let ctx = Context::with_backend(piv, None, false).unwrap();
396 let mut report = CardReport::build(&ctx, 0x82);
397 report.key_check = Some(SlotKeyCheck::Mismatch);
398 let slot = &report.items(None)[3];
399 assert_eq!(
400 slot.text,
401 "EC P-256, generated on card, certificate CN=YBLOB ECCP256"
402 );
403 assert_eq!(report.severity(), Severity::Error);
404 }
405
406 #[test]
407 fn store_presence() {
408 let piv = VirtualPiv::new();
409 let reader = piv.reader_name();
410 assert!(matches!(
411 StorePresence::probe(&reader, &piv),
412 StorePresence::None
413 ));
414 piv.write_object(&reader, OBJECT_ID_ZERO, b"garbage", MGMT)
415 .unwrap();
416 assert!(matches!(
417 StorePresence::probe(&reader, &piv),
418 StorePresence::Unreadable(_)
419 ));
420 Store::format(&reader, &piv, 4, 0x82, MGMT).unwrap();
421 assert!(matches!(
422 StorePresence::probe(&reader, &piv),
423 StorePresence::Present(_)
424 ));
425 }
426}