totp_input.rs 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. //! Fuzz TOTP verifier construction and the ±1-step window (`crypto::totp`).
  2. //!
  3. //! The input is a secret line, a newline, then the presented code. A malformed
  4. //! stored secret is always [`TotpError::InvalidSecret`], never a wrong-code
  5. //! `None`. A matched step must sit within one step of the counter for the fixed
  6. //! `now`, and it must be the step that generates the presented code. A
  7. //! 6-digit collision across neighbouring steps is therefore not a failure.
  8. //! [`normalize_recovery_code`] is idempotent, strips separators, and leaves no
  9. //! ASCII lowercase (Unicode characters without an uppercase mapping can stay
  10. //! lowercase).
  11. #![no_main]
  12. use d_oauth::crypto::totp::{build_totp, normalize_recovery_code, verify_window};
  13. use libfuzzer_sys::fuzz_target;
  14. /// Fixed instant: 2023-11-14T22:13:20Z, never the wall clock.
  15. const NOW: u64 = 1_700_000_000;
  16. /// RFC 6238 default step in seconds.
  17. const STEP: u64 = 30;
  18. fuzz_target!(|data: &[u8]| {
  19. let Ok(text) = core::str::from_utf8(data) else {
  20. return;
  21. };
  22. let (secret, code) = match text.split_once('\n') {
  23. Some((secret, code)) => (secret, code),
  24. None => (text, ""),
  25. };
  26. // Normalization is idempotent, separator-free, and uppercase.
  27. let normalized = normalize_recovery_code(code);
  28. assert_eq!(
  29. normalized,
  30. normalize_recovery_code(&normalized),
  31. "recovery normalization is not idempotent"
  32. );
  33. assert!(
  34. normalized.chars().all(|c| !c.is_whitespace() && c != '-'),
  35. "normalized recovery code kept a separator"
  36. );
  37. assert!(
  38. normalized.chars().all(|c| !c.is_ascii_lowercase()),
  39. "normalized recovery code kept ASCII lowercase"
  40. );
  41. match build_totp(secret) {
  42. Err(_) => {
  43. // A malformed stored secret is an error once the presented code reaches
  44. // secret parsing; a non-conforming code is rejected as a wrong code
  45. // (`Ok(None)`) before the secret is inspected, so probe with a shaped
  46. // code to exercise the error path deterministically.
  47. assert!(
  48. verify_window(secret, "000000", NOW).is_err(),
  49. "malformed secret slipped through verify_window"
  50. );
  51. // The fuzzed code is still processed without panicking.
  52. let _ = verify_window(secret, code, NOW);
  53. }
  54. Ok(totp) => {
  55. let counter = NOW / STEP;
  56. if let Ok(Some(step)) = verify_window(secret, code, NOW) {
  57. assert!(
  58. step.abs_diff(counter) <= 1,
  59. "matched step is outside the ±1 window"
  60. );
  61. let generated = totp.generate(step.saturating_mul(STEP)).to_string();
  62. assert_eq!(
  63. generated,
  64. code.trim(),
  65. "matched step did not generate the presented code"
  66. );
  67. }
  68. }
  69. }
  70. });