lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }