81 lines
4.7 KiB
Rust
81 lines
4.7 KiB
Rust
// file: crates/ksp-wallet-lib/unit_tests/security.rs
|
|
// version: 3
|
|
|
|
//! Adversarial security canaries for native `.kspwallet` V1.
|
|
|
|
use base64::Engine; // rust-rules: trait-import
|
|
|
|
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
|
|
|
|
#[test]
|
|
fn owner_signed_regions_reject_canonical_tampering_before_unlock() {
|
|
for pointer in [
|
|
"/key_slots/0/kdf/salt",
|
|
"/key_slots/0/wrap/ciphertext",
|
|
"/owner_control/ciphertext",
|
|
"/metadata/ciphertext",
|
|
"/secret/ciphertext",
|
|
"/state_signature/signature",
|
|
] {
|
|
let tampered = tamper_base64url_field(FULL_VECTOR, pointer);
|
|
let error = crate::inspect_locked_wallet_v1(tampered.as_slice()).expect_err("OWNER-signed canonical tampering must be rejected");
|
|
assert_eq!(error.code(), crate::ERROR_CODE_AUTHENTICATION_FAILED, "unexpected error code for tampered field {pointer}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn owner_signature_verification_precedes_owner_and_view_password_kdf() {
|
|
let tampered = tamper_base64url_field(FULL_VECTOR, "/metadata/ciphertext");
|
|
let runtime = runtime();
|
|
let owner = runtime.block_on(crate::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(std::string::String::new())));
|
|
assert_eq!(owner.expect_err("tampered state must fail before an empty OWNER password reaches Argon2").code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
|
|
let view = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(std::string::String::new())));
|
|
assert_eq!(view.expect_err("tampered state must fail before an empty VIEW password reaches Argon2").code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
|
|
}
|
|
|
|
#[test]
|
|
fn view_credential_tampering_does_not_forge_owner_state_and_cannot_unlock_view() {
|
|
let tampered = tamper_base64url_field(FULL_VECTOR, "/key_slots/1/wrap/ciphertext");
|
|
let locked = crate::inspect_locked_wallet_v1(tampered.as_slice()).expect("VIEW wrapping credentials are intentionally outside the OWNER state transcript");
|
|
assert!(locked.view_enabled());
|
|
let runtime = runtime();
|
|
let owner =
|
|
runtime.block_on(crate::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))));
|
|
assert!(owner.is_ok(), "VIEW credential tampering must not invalidate OWNER unlock");
|
|
let view = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
|
|
assert_eq!(view.expect_err("tampered VIEW wrapping must not unlock metadata").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
|
|
}
|
|
|
|
#[test]
|
|
fn unlock_failures_do_not_echo_password_material() {
|
|
let owner_canary = "OWNER-PASSWORD-LEAK-CANARY";
|
|
let view_canary = "VIEW-PASSWORD-LEAK-CANARY";
|
|
let runtime = runtime();
|
|
let owner = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from(owner_canary))));
|
|
let owner_error = owner.expect_err("wrong OWNER password must fail");
|
|
assert_eq!(owner_error.code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
|
|
assert!(!owner_error.to_string().contains(owner_canary));
|
|
assert!(!format!("{owner_error:?}").contains(owner_canary));
|
|
let view = runtime.block_on(crate::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(std::string::String::from(view_canary))));
|
|
let view_error = view.expect_err("wrong VIEW password must fail");
|
|
assert_eq!(view_error.code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
|
|
assert!(!view_error.to_string().contains(view_canary));
|
|
assert!(!format!("{view_error:?}").contains(view_canary));
|
|
}
|
|
|
|
fn tamper_base64url_field(source: &[u8], pointer: &str) -> std::vec::Vec<u8> {
|
|
let mut value: serde_json::Value = serde_json::from_slice(source).expect("security fixture must be valid JSON");
|
|
let target = value.pointer_mut(pointer).expect("security fixture pointer must exist");
|
|
let encoded = target.as_str().expect("security fixture target must be a Base64url string");
|
|
let mut decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes()).expect("security fixture Base64url must decode");
|
|
let first = decoded.first_mut().expect("security fixture Base64url field must not be empty");
|
|
*first ^= 0x01;
|
|
*target = serde_json::Value::String(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(decoded.as_slice()));
|
|
zeroize::Zeroize::zeroize(decoded.as_mut_slice());
|
|
return serde_json::to_vec(&value).expect("tampered security fixture must serialize");
|
|
}
|
|
|
|
fn runtime() -> tokio::runtime::Runtime {
|
|
return tokio::runtime::Builder::new_current_thread().build().expect("security test runtime must be creatable");
|
|
}
|