tests.rs (12085B)
1 use super::{ 2 LocalWrappedKeySource, RuntimeProtectedFileError, WRAPPED_KEY_VERSION, local_wrapping_key_path, 3 open_local_secret_file, seal_local_secret_file, 4 }; 5 use chacha20poly1305::aead::Error as AeadError; 6 use radroots_secret_vault::RadrootsSecretKeyWrapping; 7 use std::path::PathBuf; 8 use std::sync::{Mutex, OnceLock}; 9 10 fn cwd_lock() -> &'static Mutex<()> { 11 static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); 12 LOCK.get_or_init(|| Mutex::new(())) 13 } 14 15 #[test] 16 fn secret_file_round_trips_with_sidecar_key() { 17 let temp = tempfile::tempdir().expect("tempdir"); 18 let path = temp.path().join("identity.secret.json"); 19 20 seal_local_secret_file( 21 &path, 22 "runtime_test_identity", 23 br#"{"secret_key":"secret"}"#, 24 ) 25 .expect("seal local secret file"); 26 27 let payload = 28 open_local_secret_file(&path, "runtime_test_identity").expect("open local secret file"); 29 assert_eq!(payload, br#"{"secret_key":"secret"}"#); 30 assert!(local_wrapping_key_path(&path).is_file()); 31 } 32 33 #[test] 34 fn secret_file_open_fails_when_wrapping_key_is_missing() { 35 let temp = tempfile::tempdir().expect("tempdir"); 36 let path = temp.path().join("identity.secret.json"); 37 38 seal_local_secret_file(&path, "runtime_test_identity", b"payload") 39 .expect("seal local secret file"); 40 std::fs::remove_file(local_wrapping_key_path(&path)).expect("remove wrapping key"); 41 42 let err = 43 open_local_secret_file(&path, "runtime_test_identity").expect_err("missing wrapping key"); 44 assert!(err.to_string().contains("identity.secret.json")); 45 } 46 47 #[test] 48 fn secret_file_open_fails_when_key_slot_does_not_match() { 49 let temp = tempfile::tempdir().expect("tempdir"); 50 let path = temp.path().join("identity.secret.json"); 51 52 seal_local_secret_file(&path, "runtime_test_identity", b"payload") 53 .expect("seal local secret file"); 54 55 let err = 56 open_local_secret_file(&path, "unexpected_slot").expect_err("slot mismatch should fail"); 57 assert!( 58 err.to_string() 59 .contains("expected key slot unexpected_slot") 60 ); 61 } 62 63 #[test] 64 fn local_wrapped_key_source_reuses_existing_key_file() { 65 let temp = tempfile::tempdir().expect("tempdir"); 66 let path = temp.path().join("identity.secret.json"); 67 let key_path = local_wrapping_key_path(&path); 68 let expected = [7_u8; super::RADROOTS_PROTECTED_STORE_KEY_LENGTH]; 69 std::fs::write(&key_path, expected).expect("write sidecar key"); 70 71 let source = LocalWrappedKeySource::new(&path); 72 let loaded = source 73 .load_or_create_wrapping_key() 74 .expect("existing key should be reused"); 75 76 assert_eq!(loaded, expected); 77 } 78 79 #[test] 80 fn local_wrapped_key_source_rejects_invalid_key_length() { 81 let temp = tempfile::tempdir().expect("tempdir"); 82 let path = temp.path().join("identity.secret.json"); 83 let key_path = local_wrapping_key_path(&path); 84 std::fs::write( 85 &key_path, 86 [7_u8; super::RADROOTS_PROTECTED_STORE_KEY_LENGTH - 1], 87 ) 88 .expect("write short sidecar key"); 89 90 let source = LocalWrappedKeySource::new(&path); 91 let err = source 92 .load_wrapping_key() 93 .expect_err("short wrapping key must fail"); 94 95 assert!( 96 err.to_string().contains("invalid length"), 97 "unexpected error: {err}" 98 ); 99 } 100 101 #[test] 102 fn local_wrapped_key_source_rejects_truncated_invalid_and_tampered_wrapped_keys() { 103 let temp = tempfile::tempdir().expect("tempdir"); 104 let path = temp.path().join("identity.secret.json"); 105 let source = LocalWrappedKeySource::new(&path); 106 107 let wrapped = source 108 .wrap_data_key("runtime_test_identity", b"payload") 109 .expect("wrap succeeds"); 110 111 let err = source 112 .unwrap_data_key( 113 "runtime_test_identity", 114 &wrapped[..=super::RADROOTS_PROTECTED_STORE_NONCE_LENGTH], 115 ) 116 .expect_err("truncated wrapped key must fail"); 117 assert!(err.to_string().contains("truncated")); 118 119 let mut invalid_version = wrapped.clone(); 120 invalid_version[0] = WRAPPED_KEY_VERSION + 1; 121 let err = source 122 .unwrap_data_key("runtime_test_identity", &invalid_version) 123 .expect_err("invalid wrapped key version must fail"); 124 assert!( 125 err.to_string() 126 .contains("unsupported wrapped protected secret data key version") 127 ); 128 129 let mut tampered = wrapped; 130 let last = tampered.len() - 1; 131 tampered[last] ^= 0x01; 132 let err = source 133 .unwrap_data_key("runtime_test_identity", &tampered) 134 .expect_err("tampered ciphertext must fail"); 135 assert!( 136 err.to_string() 137 .contains("failed to unwrap protected secret data key") 138 ); 139 } 140 141 #[test] 142 fn seal_local_secret_file_reports_create_dir_failure() { 143 let temp = tempfile::tempdir().expect("tempdir"); 144 let blocked_parent = temp.path().join("not-a-dir"); 145 std::fs::write(&blocked_parent, b"blocker").expect("write blocker file"); 146 let path = blocked_parent.join("identity.secret.json"); 147 148 let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") 149 .expect_err("parent file must block directory creation"); 150 151 match &err { 152 RuntimeProtectedFileError::CreateDir { path: err_path, .. } => { 153 assert_eq!(err_path, &blocked_parent); 154 } 155 other => panic!("unexpected create-dir error: {other}"), 156 } 157 } 158 159 #[test] 160 fn seal_local_secret_file_reports_seal_failure_for_invalid_existing_wrapping_key() { 161 let temp = tempfile::tempdir().expect("tempdir"); 162 let path = temp.path().join("identity.secret.json"); 163 std::fs::write(local_wrapping_key_path(&path), [1_u8; 3]).expect("write invalid sidecar"); 164 165 let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") 166 .expect_err("invalid sidecar should fail sealing"); 167 168 match &err { 169 RuntimeProtectedFileError::Seal { 170 path: err_path, 171 message, 172 } => { 173 assert_eq!(err_path, &path); 174 assert!(!message.is_empty()); 175 } 176 other => panic!("unexpected seal error: {other}"), 177 } 178 } 179 180 #[test] 181 fn seal_local_secret_file_reports_io_error_when_target_is_directory() { 182 let temp = tempfile::tempdir().expect("tempdir"); 183 let path = temp.path().join("identity.secret.json"); 184 std::fs::create_dir(&path).expect("create directory target"); 185 186 let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") 187 .expect_err("directory target must fail write"); 188 189 match &err { 190 RuntimeProtectedFileError::Io { path: err_path, .. } => { 191 assert_eq!(err_path, &path); 192 } 193 other => panic!("unexpected io error: {other}"), 194 } 195 } 196 197 #[test] 198 fn seal_local_secret_file_reports_encode_failure() { 199 let temp = tempfile::tempdir().expect("tempdir"); 200 let path = temp.path().join("identity.secret.json"); 201 let _guard = super::test_hooks::fail_encode(); 202 203 let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") 204 .expect_err("forced encode failure must surface"); 205 206 match &err { 207 RuntimeProtectedFileError::Seal { 208 path: err_path, 209 message, 210 } => { 211 assert_eq!(err_path, &path); 212 assert!(!message.is_empty()); 213 } 214 other => panic!("unexpected encode error: {other}"), 215 } 216 } 217 218 #[cfg(unix)] 219 #[test] 220 fn seal_local_secret_file_reports_permissions_failure_for_payload_file() { 221 let temp = tempfile::tempdir().expect("tempdir"); 222 let path = temp.path().join("identity.secret.json"); 223 std::fs::write( 224 local_wrapping_key_path(&path), 225 [7_u8; super::RADROOTS_PROTECTED_STORE_KEY_LENGTH], 226 ) 227 .expect("write existing sidecar key"); 228 let _guard = super::test_hooks::fail_perms(); 229 230 let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") 231 .expect_err("forced permissions failure must surface"); 232 233 match &err { 234 RuntimeProtectedFileError::Permissions { 235 path: err_path, 236 message, 237 } => { 238 assert_eq!(err_path, &path); 239 assert!(!message.is_empty()); 240 } 241 other => panic!("unexpected permissions error: {other}"), 242 } 243 } 244 245 #[test] 246 fn open_local_secret_file_reports_io_error_for_missing_payload_file() { 247 let temp = tempfile::tempdir().expect("tempdir"); 248 let path = temp.path().join("missing.secret.json"); 249 250 let err = 251 open_local_secret_file(&path, "runtime_test_identity").expect_err("missing file must fail"); 252 253 match &err { 254 RuntimeProtectedFileError::Io { path: err_path, .. } => { 255 assert_eq!(err_path, &path); 256 } 257 other => panic!("unexpected open io error: {other}"), 258 } 259 } 260 261 #[test] 262 fn open_local_secret_file_reports_decode_error_for_invalid_payload() { 263 let temp = tempfile::tempdir().expect("tempdir"); 264 let path = temp.path().join("identity.secret.json"); 265 std::fs::write(&path, b"not-json").expect("write invalid payload"); 266 267 let err = open_local_secret_file(&path, "runtime_test_identity") 268 .expect_err("invalid json payload must fail"); 269 270 match &err { 271 RuntimeProtectedFileError::Decode { path: err_path, .. } => { 272 assert_eq!(err_path, &path); 273 } 274 other => panic!("unexpected decode error: {other}"), 275 } 276 } 277 278 #[test] 279 fn local_wrapped_key_source_creates_key_for_parentless_paths() { 280 let _guard = cwd_lock().lock().expect("cwd lock"); 281 let temp = tempfile::tempdir().expect("tempdir"); 282 let original = std::env::current_dir().expect("current dir"); 283 std::env::set_current_dir(temp.path()).expect("switch cwd"); 284 285 let path = PathBuf::from("identity.secret.json"); 286 let source = LocalWrappedKeySource::new(&path); 287 let loaded = source 288 .load_or_create_wrapping_key() 289 .expect("parentless path should create key"); 290 291 assert_eq!(loaded.len(), super::RADROOTS_PROTECTED_STORE_KEY_LENGTH); 292 assert!(local_wrapping_key_path(&path).is_file()); 293 294 std::env::set_current_dir(original).expect("restore cwd"); 295 } 296 297 #[test] 298 fn seal_local_secret_file_allows_parentless_paths() { 299 let _guard = cwd_lock().lock().expect("cwd lock"); 300 let temp = tempfile::tempdir().expect("tempdir"); 301 let original = std::env::current_dir().expect("current dir"); 302 std::env::set_current_dir(temp.path()).expect("switch cwd"); 303 304 let path = PathBuf::from("identity.secret.json"); 305 seal_local_secret_file(&path, "runtime_test_identity", b"payload") 306 .expect("parentless path should seal"); 307 let payload = 308 open_local_secret_file(&path, "runtime_test_identity").expect("parentless path opens"); 309 assert_eq!(payload, b"payload"); 310 311 std::env::set_current_dir(original).expect("restore cwd"); 312 } 313 314 #[test] 315 fn secret_file_helper_errors_preserve_expected_messages() { 316 let entropy = super::entropy_unavailable_error(getrandom::Error::UNSUPPORTED); 317 assert_eq!( 318 entropy.to_string(), 319 "secret vault access error: entropy unavailable" 320 ); 321 322 let wrap = super::wrap_data_key_error(AeadError); 323 assert_eq!( 324 wrap.to_string(), 325 "secret vault access error: failed to wrap protected secret data key" 326 ); 327 } 328 329 #[test] 330 fn secret_file_runtime_error_helpers_preserve_path_and_message() { 331 let path = PathBuf::from("identity.secret.json"); 332 333 let seal = super::seal_error(&path, "seal failed".to_string()); 334 match seal { 335 RuntimeProtectedFileError::Seal { 336 path: err_path, 337 message, 338 } => { 339 assert_eq!(err_path, path); 340 assert_eq!(message, "seal failed"); 341 } 342 other => panic!("unexpected seal helper error: {other}"), 343 } 344 345 let permissions = super::permissions_error(&path, "chmod failed".to_string()); 346 match permissions { 347 RuntimeProtectedFileError::Permissions { 348 path: err_path, 349 message, 350 } => { 351 assert_eq!(err_path, path); 352 assert_eq!(message, "chmod failed"); 353 } 354 other => panic!("unexpected permissions helper error: {other}"), 355 } 356 }