//! `provision keygen` contract: fresh P-256 key, `0600` file, stable //! thumbprint, and no silent overwrite of an in-use key. use std::path::PathBuf; use provision::keygen::{DEFAULT_DPOP_KEY_PATH, generate_key}; fn unique_dir(tag: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!("provision-keygen-{}-{tag}", std::process::id())); std::fs::create_dir_all(&dir).expect("test tempdir must be creatable"); dir } #[test] fn keygen_writes_0600_reloads_with_stable_thumbprint() { let dir = unique_dir("roundtrip"); let path = dir.join("dpop-key.json"); let first_jkt = generate_key(&path, false).expect("first keygen must succeed"); assert!(path.exists(), "key file must exist after keygen"); let loaded = oauth_core::dpop::key::DpopKey::load_from_file(&path).expect("generated file must reload"); assert_eq!(loaded.thumbprint().expect("thumbprint"), first_jkt); let raw = std::fs::read_to_string(&path).expect("key file must be readable"); let value: serde_json::Value = serde_json::from_str(&raw).expect("key file must be JSON"); assert_eq!(value.get("kty").and_then(|v| v.as_str()), Some("EC")); assert!(value.get("d").is_some(), "persisted JWK carries the scalar"); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mode = std::fs::metadata(&path) .expect("metadata") .permissions() .mode() & 0o777; assert_eq!(mode, 0o600, "DPoP key file must be owner-only"); } std::fs::remove_file(&path).expect("cleanup"); std::fs::remove_dir(&dir).expect("cleanup"); } #[test] fn keygen_refuses_overwrite_without_force_and_rotates_with_force() { let dir = unique_dir("overwrite"); let path = dir.join("dpop-key.json"); let first_jkt = generate_key(&path, false).expect("first keygen must succeed"); let before = std::fs::read(&path).expect("key file must be readable"); let err = generate_key(&path, false).expect_err("second keygen must fail"); assert!( matches!(err, provision::keygen::KeygenError::Exists { .. }), "rerun without --force must be a typed Exists error" ); let kept = std::fs::read(&path).expect("key file must still be readable"); assert_eq!(kept, before, "refused rerun must leave the key untouched"); let second_jkt = generate_key(&path, true).expect("--force must rotate"); assert_ne!(first_jkt, second_jkt, "forced rerun must mint a fresh key"); let rotated = oauth_core::dpop::key::DpopKey::load_from_file(&path).expect("rotated file must reload"); assert_eq!(rotated.thumbprint().expect("thumbprint"), second_jkt); std::fs::remove_file(&path).expect("cleanup"); std::fs::remove_dir(&dir).expect("cleanup"); } #[test] fn default_dpop_key_path_matches_server_convention() { assert_eq!(DEFAULT_DPOP_KEY_PATH, "config/dpop-key.json"); }