keygen.rs 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. //! `provision keygen` contract: fresh P-256 key, `0600` file, stable
  2. //! thumbprint, and no silent overwrite of an in-use key.
  3. use std::path::PathBuf;
  4. use provision::keygen::{DEFAULT_DPOP_KEY_PATH, generate_key};
  5. fn unique_dir(tag: &str) -> PathBuf {
  6. let dir = std::env::temp_dir().join(format!("provision-keygen-{}-{tag}", std::process::id()));
  7. std::fs::create_dir_all(&dir).expect("test tempdir must be creatable");
  8. dir
  9. }
  10. #[test]
  11. fn keygen_writes_0600_reloads_with_stable_thumbprint() {
  12. let dir = unique_dir("roundtrip");
  13. let path = dir.join("dpop-key.json");
  14. let first_jkt = generate_key(&path, false).expect("first keygen must succeed");
  15. assert!(path.exists(), "key file must exist after keygen");
  16. let loaded =
  17. oauth_core::dpop::key::DpopKey::load_from_file(&path).expect("generated file must reload");
  18. assert_eq!(loaded.thumbprint().expect("thumbprint"), first_jkt);
  19. let raw = std::fs::read_to_string(&path).expect("key file must be readable");
  20. let value: serde_json::Value = serde_json::from_str(&raw).expect("key file must be JSON");
  21. assert_eq!(value.get("kty").and_then(|v| v.as_str()), Some("EC"));
  22. assert!(value.get("d").is_some(), "persisted JWK carries the scalar");
  23. #[cfg(unix)]
  24. {
  25. use std::os::unix::fs::PermissionsExt;
  26. let mode = std::fs::metadata(&path)
  27. .expect("metadata")
  28. .permissions()
  29. .mode()
  30. & 0o777;
  31. assert_eq!(mode, 0o600, "DPoP key file must be owner-only");
  32. }
  33. std::fs::remove_file(&path).expect("cleanup");
  34. std::fs::remove_dir(&dir).expect("cleanup");
  35. }
  36. #[test]
  37. fn keygen_refuses_overwrite_without_force_and_rotates_with_force() {
  38. let dir = unique_dir("overwrite");
  39. let path = dir.join("dpop-key.json");
  40. let first_jkt = generate_key(&path, false).expect("first keygen must succeed");
  41. let before = std::fs::read(&path).expect("key file must be readable");
  42. let err = generate_key(&path, false).expect_err("second keygen must fail");
  43. assert!(
  44. matches!(err, provision::keygen::KeygenError::Exists { .. }),
  45. "rerun without --force must be a typed Exists error"
  46. );
  47. let kept = std::fs::read(&path).expect("key file must still be readable");
  48. assert_eq!(kept, before, "refused rerun must leave the key untouched");
  49. let second_jkt = generate_key(&path, true).expect("--force must rotate");
  50. assert_ne!(first_jkt, second_jkt, "forced rerun must mint a fresh key");
  51. let rotated =
  52. oauth_core::dpop::key::DpopKey::load_from_file(&path).expect("rotated file must reload");
  53. assert_eq!(rotated.thumbprint().expect("thumbprint"), second_jkt);
  54. std::fs::remove_file(&path).expect("cleanup");
  55. std::fs::remove_dir(&dir).expect("cleanup");
  56. }
  57. #[test]
  58. fn default_dpop_key_path_matches_server_convention() {
  59. assert_eq!(DEFAULT_DPOP_KEY_PATH, "config/dpop-key.json");
  60. }