1use super::{CardError, CardOp, PcscCode, YbError};
11use std::error::Error as StdError;
12
13const ISSUES_URL: &str = "https://github.com/douzebis/yb/issues";
14
15#[derive(Clone, Copy)]
17enum OpPat {
18 Any,
19 Is(fn(&CardOp) -> bool),
20}
21
22impl OpPat {
23 fn matches(self, op: &CardOp) -> bool {
24 match self {
25 Self::Any => true,
26 Self::Is(pred) => pred(op),
27 }
28 }
29}
30
31struct Entry {
35 op: OpPat,
36 sw_value: u16,
37 mask: u16,
38 what: &'static str,
39 why: &'static str,
40 fix: &'static str,
41}
42
43const EXACT: u16 = 0xFFFF;
44
45static CATALOG: &[Entry] = &[
47 Entry {
48 op: OpPat::Is(|op| *op == CardOp::VerifyPin),
49 sw_value: 0x63C0,
50 mask: 0xFFF0,
51 what: "wrong PIN",
52 why: "{tries}",
53 fix: "",
54 },
55 Entry {
56 op: OpPat::Is(|op| *op == CardOp::VerifyPin),
57 sw_value: 0x6983,
58 mask: EXACT,
59 what: "the PIN is blocked",
60 why: "too many wrong attempts",
61 fix: "unblock it with the PUK (`ykman piv access unblock-pin`); if the PUK is \
62 blocked too, only a PIV reset helps, and it erases everything",
63 },
64 Entry {
65 op: OpPat::Is(|op| *op == CardOp::ChangePin),
66 sw_value: 0x63C0,
67 mask: 0xFFF0,
68 what: "wrong current PIN",
69 why: "{tries}",
70 fix: "",
71 },
72 Entry {
73 op: OpPat::Is(|op| *op == CardOp::ChangePuk),
74 sw_value: 0x63C0,
75 mask: 0xFFF0,
76 what: "wrong current PUK",
77 why: "{tries}",
78 fix: "",
79 },
80 Entry {
81 op: OpPat::Is(|op| matches!(op, CardOp::ChangePin | CardOp::ChangePuk)),
82 sw_value: 0x6983,
83 mask: EXACT,
84 what: "the {cred} is blocked",
85 why: "too many wrong attempts",
86 fix: "",
87 },
88 Entry {
89 op: OpPat::Is(|op| matches!(op, CardOp::ChangePin | CardOp::ChangePuk)),
90 sw_value: 0x6985,
91 mask: EXACT,
92 what: "the new {cred} does not meet this YubiKey's complexity requirement",
93 why: "the YubiKey enforces a PIN complexity policy",
94 fix: "choose a less predictable value (no repeated or sequential digits, \
95 not a common PIN)",
96 },
97 Entry {
98 op: OpPat::Is(|op| *op == CardOp::MgmtAuth),
99 sw_value: 0x6A80,
100 mask: EXACT,
101 what: "cannot authenticate with the management key",
102 why: "the YubiKey rejected the {algo} algorithm yb used",
103 fix: "`yb fsck` shows the card's management key algorithm",
104 },
105 Entry {
106 op: OpPat::Is(|op| *op == CardOp::MgmtAuth),
107 sw_value: 0x6982,
108 mask: EXACT,
109 what: "wrong management key",
110 why: "the key from YB_MANAGEMENT_KEY or from PRINTED is not the YubiKey's \
111 management key",
112 fix: "check YB_MANAGEMENT_KEY; `yb fsck` shows whether the YubiKey keeps its \
113 management key",
114 },
115 Entry {
116 op: OpPat::Is(|op| *op == CardOp::ReadObject(crate::auxiliaries::OBJ_PRINTED)),
117 sw_value: 0x6A82,
118 mask: EXACT,
119 what: "the management key is not stored on the YubiKey",
120 why: "the YubiKey does not keep its management key, or was set up by another tool",
121 fix: "set YB_MANAGEMENT_KEY, or have yb keep a new one on the YubiKey with \
122 `yb rotate-management-key`",
123 },
124 Entry {
125 op: OpPat::Is(|op| matches!(op, CardOp::ReadCertificate(_))),
126 sw_value: 0x6A82,
127 mask: EXACT,
128 what: "no key in slot {slot}",
129 why: "this YubiKey has not been set up for yb",
130 fix: "`yb format --generate`",
131 },
132 Entry {
133 op: OpPat::Is(|op| matches!(op, CardOp::WriteObject(_))),
134 sw_value: 0x6A84,
135 mask: EXACT,
136 what: "the YubiKey's storage is full",
137 why: "the PIV area's ~51 KB are used up",
138 fix: "`yb fsck --nvm`, then remove blobs",
139 },
140 Entry {
141 op: OpPat::Is(|op| matches!(op, CardOp::WriteObject(_))),
142 sw_value: 0x6982,
143 mask: EXACT,
144 what: "the YubiKey refused the write",
145 why: "management key authentication did not happen or was lost",
146 fix: "this is likely a yb bug: please report it",
147 },
148 Entry {
149 op: OpPat::Is(|op| {
150 matches!(
151 op,
152 CardOp::GenerateKey(_) | CardOp::Sign(_) | CardOp::Ecdh(_)
153 )
154 }),
155 sw_value: 0x6982,
156 mask: EXACT,
157 what: "the YubiKey requires the PIN or a touch for this key",
158 why: "the PIN was not verified, or the key requires a touch",
159 fix: "run the command again, and touch the YubiKey if it blinks",
160 },
161 Entry {
162 op: OpPat::Is(|op| matches!(op, CardOp::Sign(_) | CardOp::Ecdh(_))),
163 sw_value: 0x6A80,
164 mask: EXACT,
165 what: "the key in slot {slot} cannot do this operation",
166 why: "it is not an EC P-256 key",
167 fix: "`yb format --generate` (erases the store)",
168 },
169 Entry {
170 op: OpPat::Any,
171 sw_value: 0x6D00,
172 mask: EXACT,
173 what: "this YubiKey does not support this operation",
174 why: "its firmware is too old for it",
175 fix: "",
176 },
177 Entry {
178 op: OpPat::Is(|op| *op == CardOp::SelectPiv),
179 sw_value: 0x6A82,
180 mask: EXACT,
181 what: "the PIV application is not available",
182 why: "PIV is disabled on this YubiKey, or the device is not a YubiKey",
183 fix: "`ykman config usb --enable PIV`",
184 },
185];
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct Explanation {
190 pub what: String,
191 pub why: Option<String>,
192 pub fix: Option<String>,
193 pub details: Option<String>,
195}
196
197#[derive(Debug, Clone, Default)]
200pub struct RenderEnv {
201 pub firmware: Option<String>,
202 pub yb_version: Option<String>,
203}
204
205pub fn explain_pcsc(code: &PcscCode) -> Option<Explanation> {
207 let (what, fix) = match code {
208 PcscCode::NoService => (
209 "the smart card service is not running",
210 "start pcscd (`systemctl start pcscd.socket`); on NixOS, set \
211 `services.pcscd.enable = true`",
212 ),
213 PcscCode::NoReaders => (
214 "no YubiKey found",
215 "check that it is plugged in and that its CCID interface is enabled \
216 (`ykman config usb`); in a VM, check the USB passthrough (`lsusb` shows `1050:…`)",
217 ),
218 PcscCode::SharingViolation => (
219 "another program is using the YubiKey",
220 "usually gpg's scdaemon: `gpgconf --kill scdaemon`",
221 ),
222 PcscCode::RemovedCard | PcscCode::ResetCard => (
223 "the YubiKey was removed or reset during the operation",
224 "reconnect it; after a write command, run `yb fsck` to check the store",
225 ),
226 PcscCode::Other(_) => return None,
227 };
228 Some(Explanation {
229 what: what.to_owned(),
230 why: None,
231 fix: Some(fix.to_owned()),
232 details: None,
233 })
234}
235
236fn substitute(template: &str, op: &CardOp, sw: u16, err: &CardError) -> String {
237 let cred = if *op == CardOp::ChangePuk {
238 "PUK"
239 } else {
240 "PIN"
241 };
242 let tries = (sw & 0x0F) as u8;
243 let tries_text = match tries {
244 0 => format!("no attempts left: the {cred} is now blocked"),
245 1 => format!("1 attempt left; one more failure blocks the {cred}"),
246 n => format!("{n} attempts left before the {cred} is blocked"),
247 };
248 let algo = match err {
249 CardError::Status { ctx, .. } => ctx.algo.map(|a| a.to_string()),
250 _ => None,
251 }
252 .unwrap_or_else(|| "management key".to_owned());
253 let slot = match op {
254 CardOp::ReadCertificate(s) | CardOp::Sign(s) | CardOp::Ecdh(s) | CardOp::GenerateKey(s) => {
255 format!("0x{s:02x}")
256 }
257 _ => "the store slot".to_owned(),
258 };
259 template
260 .replace("{tries}", &tries_text)
261 .replace("{cred}", cred)
262 .replace("{algo}", &algo)
263 .replace("{slot}", &slot)
264}
265
266fn non_empty(s: String) -> Option<String> {
267 (!s.is_empty()).then_some(s)
268}
269
270fn explain_card(err: &CardError) -> Option<Explanation> {
272 let details = Some(err.details());
273 let (op, sw) = match err {
274 CardError::Status { op, sw, .. } => (*op, *sw),
275 CardError::Protocol {
278 op: CardOp::MgmtAuth,
279 ..
280 } => (CardOp::MgmtAuth, 0x6982),
281 CardError::Protocol { .. } => return None,
282 CardError::Pcsc { code, .. } => {
283 return explain_pcsc(code).map(|e| Explanation { details, ..e });
284 }
285 };
286 let entry = CATALOG
287 .iter()
288 .find(|e| e.op.matches(&op) && sw & e.mask == e.sw_value)?;
289 Some(Explanation {
290 what: substitute(entry.what, &op, sw, err),
291 why: non_empty(substitute(entry.why, &op, sw, err)),
292 fix: non_empty(substitute(entry.fix, &op, sw, err)),
293 details,
294 })
295}
296
297fn unexpected(err: &CardError, env: &RenderEnv) -> Explanation {
300 let op = match err {
301 CardError::Status { op, .. } | CardError::Protocol { op, .. } => op.describe(),
302 CardError::Pcsc { .. } => "the operation".to_owned(),
303 };
304 let mut details = err.details();
305 if let CardError::Status { ctx, .. } = err {
306 if let Some(algo) = ctx.algo {
307 details.push_str(&format!("; management key algorithm {algo}"));
308 }
309 }
310 if let Some(ref fw) = env.firmware {
311 details.push_str(&format!("; firmware {fw}"));
312 }
313 if let Some(ref v) = env.yb_version {
314 details.push_str(&format!("; yb {v}"));
315 }
316 Explanation {
317 what: format!("the YubiKey rejected {op}"),
318 why: Some(format!(
319 "this is unexpected — please report it at {ISSUES_URL} with the details below"
320 )),
321 fix: None,
322 details: Some(details),
323 }
324}
325
326fn sentence(s: &str) -> String {
327 let mut chars = s.chars();
328 let mut out: String = match chars.next() {
329 Some(c) => c.to_uppercase().chain(chars).collect(),
330 None => String::new(),
331 };
332 if !out.ends_with(['.', '!', '?']) {
333 out.push('.');
334 }
335 out
336}
337
338fn with_period(s: &str) -> String {
339 if s.ends_with(['.', '!', '?']) {
340 s.to_owned()
341 } else {
342 format!("{s}.")
343 }
344}
345
346pub fn render(err: &anyhow::Error, env: &RenderEnv) -> String {
352 let chain: Vec<&(dyn StdError + 'static)> = err.chain().collect();
353 let typed = chain.iter().enumerate().find_map(|(i, e)| {
354 if let Some(c) = e.downcast_ref::<CardError>() {
355 Some((i, explain_card(c).unwrap_or_else(|| unexpected(c, env))))
356 } else {
357 e.downcast_ref::<YbError>().map(|y| {
358 (
359 i,
360 Explanation {
361 what: y.what.clone(),
362 why: y.why.clone(),
363 fix: y.fix.clone(),
364 details: None,
365 },
366 )
367 })
368 }
369 });
370 let Some((index, exp)) = typed else {
371 return format!("Error: {err:#}");
372 };
373
374 let mut lines = Vec::new();
375 if index == 0 {
376 lines.push(format!("Error: {}", with_period(&exp.what)));
377 if let Some(ref why) = exp.why {
378 lines.push(format!(" {}", sentence(why)));
379 }
380 } else {
381 let outer: Vec<String> = chain[..index].iter().map(|e| e.to_string()).collect();
382 lines.push(format!("Error: {}", outer.join(": ")));
383 let cause = match exp.why {
384 Some(ref why) => format!("{} ({why})", exp.what),
385 None => exp.what.clone(),
386 };
387 lines.push(format!(" Cause: {}", with_period(&cause)));
388 }
389 if let Some(ref fix) = exp.fix {
390 lines.push(format!(" Try: {}", with_period(fix)));
391 }
392 if let Some(ref details) = exp.details {
393 lines.push(format!(" (details: {details})"));
394 }
395 lines.join("\n")
396}
397
398#[cfg(test)]
403mod tests {
404 use super::super::{ErrCtx, PcscOp};
405 use super::*;
406 use crate::piv::MgmtAlgo;
407 use anyhow::Context as _;
408
409 fn env() -> RenderEnv {
410 RenderEnv {
411 firmware: Some("5.4.3".to_owned()),
412 yb_version: Some("0.4.2".to_owned()),
413 }
414 }
415
416 fn status(op: CardOp, sw: u16) -> anyhow::Error {
417 let [a, b] = sw.to_be_bytes();
418 anyhow::Error::new(CardError::status(op, a, b))
419 }
420
421 fn first_and_details(e: anyhow::Error) -> (String, String) {
423 let text = render(&e, &env());
424 let first = text.lines().next().unwrap().to_owned();
425 let details = text.lines().last().unwrap().to_owned();
426 (first, details)
427 }
428
429 #[test]
430 fn every_catalog_entry_renders() {
431 let cases: &[(CardOp, u16, &str)] = &[
432 (CardOp::VerifyPin, 0x63C2, "Error: wrong PIN."),
433 (CardOp::VerifyPin, 0x6983, "Error: the PIN is blocked."),
434 (
435 CardOp::MgmtAuth,
436 0x6A80,
437 "Error: cannot authenticate with the management key.",
438 ),
439 (CardOp::MgmtAuth, 0x6982, "Error: wrong management key."),
440 (
441 CardOp::ReadObject(crate::auxiliaries::OBJ_PRINTED),
442 0x6A82,
443 "Error: the management key is not stored on the YubiKey.",
444 ),
445 (
446 CardOp::ReadCertificate(0x82),
447 0x6A82,
448 "Error: no key in slot 0x82.",
449 ),
450 (
451 CardOp::WriteObject(0x5F0003),
452 0x6A84,
453 "Error: the YubiKey's storage is full.",
454 ),
455 (
456 CardOp::WriteObject(0x5F0003),
457 0x6982,
458 "Error: the YubiKey refused the write.",
459 ),
460 (
461 CardOp::Sign(0x82),
462 0x6982,
463 "Error: the YubiKey requires the PIN or a touch for this key.",
464 ),
465 (
466 CardOp::Ecdh(0x82),
467 0x6A80,
468 "Error: the key in slot 0x82 cannot do this operation.",
469 ),
470 (
471 CardOp::GenerateKey(0x82),
472 0x6D00,
473 "Error: this YubiKey does not support this operation.",
474 ),
475 (
476 CardOp::SelectPiv,
477 0x6A82,
478 "Error: the PIV application is not available.",
479 ),
480 ];
481 for &(op, sw, expected) in cases {
482 let (first, details) = first_and_details(status(op, sw));
483 assert_eq!(first, expected, "{op:?} {sw:04X}");
484 assert_eq!(
485 details,
486 format!(" (details: {} → SW {sw:04X})", op.describe()),
487 "{op:?} {sw:04X}"
488 );
489 }
490 }
491
492 #[test]
493 fn wrong_pin_counts_down() {
494 let text = render(&status(CardOp::VerifyPin, 0x63C1), &env());
495 assert!(
496 text.contains("1 attempt left; one more failure blocks the PIN"),
497 "{text}"
498 );
499 let text = render(&status(CardOp::VerifyPin, 0x63C2), &env());
500 assert!(text.contains("2 attempts left"), "{text}");
501 }
502
503 #[test]
504 fn response_mismatch_is_a_wrong_management_key() {
505 let e = anyhow::Error::new(CardError::protocol(
506 CardOp::MgmtAuth,
507 "card response mismatch",
508 ));
509 let text = render(&e, &env());
510 assert!(text.starts_with("Error: wrong management key."), "{text}");
511 assert!(text.ends_with("(details: management key authentication → card response mismatch)"));
512 }
513
514 #[test]
515 fn pcsc_errors_are_explained() {
516 for (code, what) in [
517 (PcscCode::NoService, "the smart card service is not running"),
518 (PcscCode::NoReaders, "no YubiKey found"),
519 (
520 PcscCode::SharingViolation,
521 "another program is using the YubiKey",
522 ),
523 (
524 PcscCode::RemovedCard,
525 "the YubiKey was removed or reset during the operation",
526 ),
527 ] {
528 let e = anyhow::Error::new(CardError::Pcsc {
529 op: PcscOp::Connect,
530 code: code.clone(),
531 });
532 let text = render(&e, &env());
533 assert!(text.starts_with(&format!("Error: {what}.")), "{text}");
534 assert!(text.contains(" Try: "), "{text}");
535 assert!(text.contains(&format!("(details: connecting to the YubiKey → {code})")));
536 }
537 }
538
539 #[test]
540 fn unexpected_errors_carry_all_context() {
541 let e = anyhow::Error::new(
542 CardError::status(CardOp::WriteObject(0x5F0001), 0x6F, 0x00).with_ctx(ErrCtx {
543 algo: Some(MgmtAlgo::Aes192),
544 }),
545 );
546 assert_eq!(
547 render(&e, &env()),
548 "Error: the YubiKey rejected write object 0x5F0001.\n \
549 This is unexpected — please report it at https://github.com/douzebis/yb/issues \
550 with the details below.\n \
551 (details: write object 0x5F0001 → SW 6F00; management key algorithm AES-192; \
552 firmware 5.4.3; yb 0.4.2)"
553 );
554 }
555
556 #[test]
558 fn snapshot_standalone_card_error() {
559 let e = anyhow::Error::new(CardError::status(CardOp::MgmtAuth, 0x6A, 0x80).with_ctx(
560 ErrCtx {
561 algo: Some(MgmtAlgo::Tdes),
562 },
563 ));
564 assert_eq!(
565 render(&e, &env()),
566 "Error: cannot authenticate with the management key.\n \
567 The YubiKey rejected the 3DES algorithm yb used.\n \
568 Try: `yb fsck` shows the card's management key algorithm.\n \
569 (details: management key authentication → SW 6A80)"
570 );
571 }
572
573 #[test]
575 fn snapshot_card_error_under_context() {
576 let e = Err::<(), _>(CardError::status(CardOp::WriteObject(0x5F0003), 0x6A, 0x84))
577 .context("yb format stopped while erasing the store (completed: nothing)")
578 .unwrap_err();
579 assert_eq!(
580 render(&e, &env()),
581 "Error: yb format stopped while erasing the store (completed: nothing)\n \
582 Cause: the YubiKey's storage is full (the PIV area's ~51 KB are used up).\n \
583 Try: `yb fsck --nvm`, then remove blobs.\n \
584 (details: write object 0x5F0003 → SW 6A84)"
585 );
586 }
587
588 #[test]
589 fn yb_errors_render_without_details() {
590 let e = anyhow::Error::new(
591 YbError::new("the management key is not in PRINTED and is not the factory default")
592 .fix("set YB_MANAGEMENT_KEY"),
593 );
594 assert_eq!(
595 render(&e, &env()),
596 "Error: the management key is not in PRINTED and is not the factory default.\n \
597 Try: set YB_MANAGEMENT_KEY."
598 );
599 }
600
601 #[test]
602 fn untyped_errors_render_as_before() {
603 let e = anyhow::anyhow!("plain failure").context("outer");
604 assert_eq!(render(&e, &env()), "Error: outer: plain failure");
605 }
606}