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: "remove blobs (`yb rm`); an interrupted store or removal leaves the blobs as \
141 they were, and the next yb command frees the space it used",
142 },
143 Entry {
144 op: OpPat::Is(|op| matches!(op, CardOp::WriteObject(_))),
145 sw_value: 0x6982,
146 mask: EXACT,
147 what: "the YubiKey refused the write",
148 why: "management key authentication did not happen or was lost",
149 fix: "this is likely a yb bug: please report it",
150 },
151 Entry {
152 op: OpPat::Is(|op| {
153 matches!(
154 op,
155 CardOp::GenerateKey(_) | CardOp::Sign(_) | CardOp::Ecdh(_)
156 )
157 }),
158 sw_value: 0x6982,
159 mask: EXACT,
160 what: "the YubiKey requires the PIN or a touch for this key",
161 why: "the PIN was not verified, or the key requires a touch",
162 fix: "run the command again, and touch the YubiKey if it blinks",
163 },
164 Entry {
165 op: OpPat::Is(|op| matches!(op, CardOp::Sign(_) | CardOp::Ecdh(_))),
166 sw_value: 0x6A80,
167 mask: EXACT,
168 what: "the key in slot {slot} cannot do this operation",
169 why: "it is not an EC P-256 key",
170 fix: "`yb format --generate` (erases the store)",
171 },
172 Entry {
173 op: OpPat::Any,
174 sw_value: 0x6D00,
175 mask: EXACT,
176 what: "this YubiKey does not support this operation",
177 why: "its firmware is too old for it",
178 fix: "",
179 },
180 Entry {
181 op: OpPat::Is(|op| *op == CardOp::SelectPiv),
182 sw_value: 0x6A82,
183 mask: EXACT,
184 what: "the PIV application is not available",
185 why: "PIV is disabled on this YubiKey, or the device is not a YubiKey",
186 fix: "`ykman config usb --enable PIV`",
187 },
188];
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct Explanation {
193 pub what: String,
194 pub why: Option<String>,
195 pub fix: Option<String>,
196 pub details: Option<String>,
198}
199
200#[derive(Debug, Clone, Default)]
203pub struct RenderEnv {
204 pub firmware: Option<String>,
205 pub yb_version: Option<String>,
206}
207
208pub fn explain_pcsc(code: &PcscCode) -> Option<Explanation> {
210 let (what, fix) = match code {
211 PcscCode::NoService => (
212 "the smart card service is not running",
213 "start pcscd (`systemctl start pcscd.socket`); on NixOS, set \
214 `services.pcscd.enable = true`",
215 ),
216 PcscCode::NoReaders => (
217 "no YubiKey found",
218 "check that it is plugged in and that its CCID interface is enabled \
219 (`ykman config usb`); in a VM, check the USB passthrough (`lsusb` shows `1050:…`)",
220 ),
221 PcscCode::SharingViolation => (
222 "another program is using the YubiKey",
223 "usually gpg's scdaemon: `gpgconf --kill scdaemon`",
224 ),
225 PcscCode::RemovedCard | PcscCode::ResetCard => (
226 "the YubiKey was removed or reset during the operation",
227 "reconnect it; after a write command, run `yb fsck` to check the store",
228 ),
229 PcscCode::Other(_) => return None,
230 };
231 Some(Explanation {
232 what: what.to_owned(),
233 why: None,
234 fix: Some(fix.to_owned()),
235 details: None,
236 })
237}
238
239fn substitute(template: &str, op: &CardOp, sw: u16, err: &CardError) -> String {
240 let cred = if *op == CardOp::ChangePuk {
241 "PUK"
242 } else {
243 "PIN"
244 };
245 let tries = (sw & 0x0F) as u8;
246 let tries_text = match tries {
247 0 => format!("no attempts left: the {cred} is now blocked"),
248 1 => format!("1 attempt left; one more failure blocks the {cred}"),
249 n => format!("{n} attempts left before the {cred} is blocked"),
250 };
251 let algo = match err {
252 CardError::Status { ctx, .. } => ctx.algo.map(|a| a.to_string()),
253 _ => None,
254 }
255 .unwrap_or_else(|| "management key".to_owned());
256 let slot = match op {
257 CardOp::ReadCertificate(s) | CardOp::Sign(s) | CardOp::Ecdh(s) | CardOp::GenerateKey(s) => {
258 format!("0x{s:02x}")
259 }
260 _ => "the store slot".to_owned(),
261 };
262 template
263 .replace("{tries}", &tries_text)
264 .replace("{cred}", cred)
265 .replace("{algo}", &algo)
266 .replace("{slot}", &slot)
267}
268
269fn non_empty(s: String) -> Option<String> {
270 (!s.is_empty()).then_some(s)
271}
272
273fn explain_card(err: &CardError) -> Option<Explanation> {
275 let details = Some(err.details());
276 let (op, sw) = match err {
277 CardError::Status { op, sw, .. } => (*op, *sw),
278 CardError::Protocol {
281 op: CardOp::MgmtAuth,
282 ..
283 } => (CardOp::MgmtAuth, 0x6982),
284 CardError::Protocol { .. } => return None,
285 CardError::Pcsc { code, .. } => {
286 return explain_pcsc(code).map(|e| Explanation { details, ..e });
287 }
288 };
289 let entry = CATALOG
290 .iter()
291 .find(|e| e.op.matches(&op) && sw & e.mask == e.sw_value)?;
292 Some(Explanation {
293 what: substitute(entry.what, &op, sw, err),
294 why: non_empty(substitute(entry.why, &op, sw, err)),
295 fix: non_empty(substitute(entry.fix, &op, sw, err)),
296 details,
297 })
298}
299
300fn unexpected(err: &CardError, env: &RenderEnv) -> Explanation {
303 let op = match err {
304 CardError::Status { op, .. } | CardError::Protocol { op, .. } => op.describe(),
305 CardError::Pcsc { .. } => "the operation".to_owned(),
306 };
307 let mut details = err.details();
308 if let CardError::Status { ctx, .. } = err {
309 if let Some(algo) = ctx.algo {
310 details.push_str(&format!("; management key algorithm {algo}"));
311 }
312 }
313 if let Some(ref fw) = env.firmware {
314 details.push_str(&format!("; firmware {fw}"));
315 }
316 if let Some(ref v) = env.yb_version {
317 details.push_str(&format!("; yb {v}"));
318 }
319 Explanation {
320 what: format!("the YubiKey rejected {op}"),
321 why: Some(format!(
322 "this is unexpected — please report it at {ISSUES_URL} with the details below"
323 )),
324 fix: None,
325 details: Some(details),
326 }
327}
328
329fn sentence(s: &str) -> String {
330 let mut chars = s.chars();
331 let mut out: String = match chars.next() {
332 Some(c) => c.to_uppercase().chain(chars).collect(),
333 None => String::new(),
334 };
335 if !out.ends_with(['.', '!', '?']) {
336 out.push('.');
337 }
338 out
339}
340
341fn with_period(s: &str) -> String {
342 if s.ends_with(['.', '!', '?']) {
343 s.to_owned()
344 } else {
345 format!("{s}.")
346 }
347}
348
349pub fn render(err: &anyhow::Error, env: &RenderEnv) -> String {
355 let chain: Vec<&(dyn StdError + 'static)> = err.chain().collect();
356 let typed = chain.iter().enumerate().find_map(|(i, e)| {
357 if let Some(c) = e.downcast_ref::<CardError>() {
358 Some((i, explain_card(c).unwrap_or_else(|| unexpected(c, env))))
359 } else {
360 e.downcast_ref::<YbError>().map(|y| {
361 (
362 i,
363 Explanation {
364 what: y.what.clone(),
365 why: y.why.clone(),
366 fix: y.fix.clone(),
367 details: None,
368 },
369 )
370 })
371 }
372 });
373 let Some((index, exp)) = typed else {
374 return format!("Error: {err:#}");
375 };
376
377 let mut lines = Vec::new();
378 if index == 0 {
379 lines.push(format!("Error: {}", with_period(&exp.what)));
380 if let Some(ref why) = exp.why {
381 lines.push(format!(" {}", sentence(why)));
382 }
383 } else {
384 let outer: Vec<String> = chain[..index].iter().map(|e| e.to_string()).collect();
385 lines.push(format!("Error: {}", outer.join(": ")));
386 let cause = match exp.why {
387 Some(ref why) => format!("{} ({why})", exp.what),
388 None => exp.what.clone(),
389 };
390 lines.push(format!(" Cause: {}", with_period(&cause)));
391 }
392 if let Some(ref fix) = exp.fix {
393 lines.push(format!(" Try: {}", with_period(fix)));
394 }
395 if let Some(ref details) = exp.details {
396 lines.push(format!(" (details: {details})"));
397 }
398 lines.join("\n")
399}
400
401#[cfg(test)]
406mod tests {
407 use super::super::{ErrCtx, PcscOp};
408 use super::*;
409 use crate::piv::MgmtAlgo;
410 use anyhow::Context as _;
411
412 fn env() -> RenderEnv {
413 RenderEnv {
414 firmware: Some("5.4.3".to_owned()),
415 yb_version: Some("0.4.2".to_owned()),
416 }
417 }
418
419 fn status(op: CardOp, sw: u16) -> anyhow::Error {
420 let [a, b] = sw.to_be_bytes();
421 anyhow::Error::new(CardError::status(op, a, b))
422 }
423
424 fn first_and_details(e: anyhow::Error) -> (String, String) {
426 let text = render(&e, &env());
427 let first = text.lines().next().unwrap().to_owned();
428 let details = text.lines().last().unwrap().to_owned();
429 (first, details)
430 }
431
432 #[test]
433 fn every_catalog_entry_renders() {
434 let cases: &[(CardOp, u16, &str)] = &[
435 (CardOp::VerifyPin, 0x63C2, "Error: wrong PIN."),
436 (CardOp::VerifyPin, 0x6983, "Error: the PIN is blocked."),
437 (
438 CardOp::MgmtAuth,
439 0x6A80,
440 "Error: cannot authenticate with the management key.",
441 ),
442 (CardOp::MgmtAuth, 0x6982, "Error: wrong management key."),
443 (
444 CardOp::ReadObject(crate::auxiliaries::OBJ_PRINTED),
445 0x6A82,
446 "Error: the management key is not stored on the YubiKey.",
447 ),
448 (
449 CardOp::ReadCertificate(0x82),
450 0x6A82,
451 "Error: no key in slot 0x82.",
452 ),
453 (
454 CardOp::WriteObject(0x5F0003),
455 0x6A84,
456 "Error: the YubiKey's storage is full.",
457 ),
458 (
459 CardOp::WriteObject(0x5F0003),
460 0x6982,
461 "Error: the YubiKey refused the write.",
462 ),
463 (
464 CardOp::Sign(0x82),
465 0x6982,
466 "Error: the YubiKey requires the PIN or a touch for this key.",
467 ),
468 (
469 CardOp::Ecdh(0x82),
470 0x6A80,
471 "Error: the key in slot 0x82 cannot do this operation.",
472 ),
473 (
474 CardOp::GenerateKey(0x82),
475 0x6D00,
476 "Error: this YubiKey does not support this operation.",
477 ),
478 (
479 CardOp::SelectPiv,
480 0x6A82,
481 "Error: the PIV application is not available.",
482 ),
483 ];
484 for &(op, sw, expected) in cases {
485 let (first, details) = first_and_details(status(op, sw));
486 assert_eq!(first, expected, "{op:?} {sw:04X}");
487 assert_eq!(
488 details,
489 format!(" (details: {} → SW {sw:04X})", op.describe()),
490 "{op:?} {sw:04X}"
491 );
492 }
493 }
494
495 #[test]
496 fn wrong_pin_counts_down() {
497 let text = render(&status(CardOp::VerifyPin, 0x63C1), &env());
498 assert!(
499 text.contains("1 attempt left; one more failure blocks the PIN"),
500 "{text}"
501 );
502 let text = render(&status(CardOp::VerifyPin, 0x63C2), &env());
503 assert!(text.contains("2 attempts left"), "{text}");
504 }
505
506 #[test]
507 fn response_mismatch_is_a_wrong_management_key() {
508 let e = anyhow::Error::new(CardError::protocol(
509 CardOp::MgmtAuth,
510 "card response mismatch",
511 ));
512 let text = render(&e, &env());
513 assert!(text.starts_with("Error: wrong management key."), "{text}");
514 assert!(text.ends_with("(details: management key authentication → card response mismatch)"));
515 }
516
517 #[test]
518 fn pcsc_errors_are_explained() {
519 for (code, what) in [
520 (PcscCode::NoService, "the smart card service is not running"),
521 (PcscCode::NoReaders, "no YubiKey found"),
522 (
523 PcscCode::SharingViolation,
524 "another program is using the YubiKey",
525 ),
526 (
527 PcscCode::RemovedCard,
528 "the YubiKey was removed or reset during the operation",
529 ),
530 ] {
531 let e = anyhow::Error::new(CardError::Pcsc {
532 op: PcscOp::Connect,
533 code: code.clone(),
534 });
535 let text = render(&e, &env());
536 assert!(text.starts_with(&format!("Error: {what}.")), "{text}");
537 assert!(text.contains(" Try: "), "{text}");
538 assert!(text.contains(&format!("(details: connecting to the YubiKey → {code})")));
539 }
540 }
541
542 #[test]
543 fn unexpected_errors_carry_all_context() {
544 let e = anyhow::Error::new(
545 CardError::status(CardOp::WriteObject(0x5F0001), 0x6F, 0x00).with_ctx(ErrCtx {
546 algo: Some(MgmtAlgo::Aes192),
547 }),
548 );
549 assert_eq!(
550 render(&e, &env()),
551 "Error: the YubiKey rejected write object 0x5F0001.\n \
552 This is unexpected — please report it at https://github.com/douzebis/yb/issues \
553 with the details below.\n \
554 (details: write object 0x5F0001 → SW 6F00; management key algorithm AES-192; \
555 firmware 5.4.3; yb 0.4.2)"
556 );
557 }
558
559 #[test]
561 fn snapshot_standalone_card_error() {
562 let e = anyhow::Error::new(CardError::status(CardOp::MgmtAuth, 0x6A, 0x80).with_ctx(
563 ErrCtx {
564 algo: Some(MgmtAlgo::Tdes),
565 },
566 ));
567 assert_eq!(
568 render(&e, &env()),
569 "Error: cannot authenticate with the management key.\n \
570 The YubiKey rejected the 3DES algorithm yb used.\n \
571 Try: `yb fsck` shows the card's management key algorithm.\n \
572 (details: management key authentication → SW 6A80)"
573 );
574 }
575
576 #[test]
578 fn snapshot_card_error_under_context() {
579 let e = Err::<(), _>(CardError::status(CardOp::WriteObject(0x5F0003), 0x6A, 0x84))
580 .context("yb format stopped while erasing the store (completed: nothing)")
581 .unwrap_err();
582 assert_eq!(
583 render(&e, &env()),
584 "Error: yb format stopped while erasing the store (completed: nothing)\n \
585 Cause: the YubiKey's storage is full (the PIV area's ~51 KB are used up).\n \
586 Try: remove blobs (`yb rm`); an interrupted store or removal leaves the blobs as \
587 they were, and the next yb command frees the space it used.\n \
588 (details: write object 0x5F0003 → SW 6A84)"
589 );
590 }
591
592 #[test]
593 fn yb_errors_render_without_details() {
594 let e = anyhow::Error::new(
595 YbError::new("the management key is not in PRINTED and is not the factory default")
596 .fix("set YB_MANAGEMENT_KEY"),
597 );
598 assert_eq!(
599 render(&e, &env()),
600 "Error: the management key is not in PRINTED and is not the factory default.\n \
601 Try: set YB_MANAGEMENT_KEY."
602 );
603 }
604
605 #[test]
606 fn untyped_errors_render_as_before() {
607 let e = anyhow::anyhow!("plain failure").context("outer");
608 assert_eq!(render(&e, &env()), "Error: outer: plain failure");
609 }
610}