feat(security): release v0.7.0 with comprehensive security hardening (S-01 to S-11)
Sanctum Release / Build & Release (Windows x86_64) (push) Waiting to run
Sanctum Release / Build & Release (Windows x86_64) (push) Waiting to run
This commit is contained in:
@@ -8,7 +8,7 @@ use rand::RngCore;
|
||||
|
||||
use sanctum::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_slot0_payload,
|
||||
wrap_slot1_payload, KdfParams, CHUNK_SIZE,
|
||||
wrap_slot1_payload, KdfParams, CHUNK_SIZE, MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
use sanctum::verify::verify_container;
|
||||
@@ -31,8 +31,8 @@ async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() {
|
||||
let pass_hidden = "SuperSecretHiddenPassword2026!";
|
||||
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
@@ -270,8 +270,8 @@ async fn test_model_a_container_file_size_invariance() {
|
||||
let pass_hidden = "HiddenPass2026!";
|
||||
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
@@ -369,8 +369,8 @@ async fn test_carrier_file_drop_and_append_mode() {
|
||||
let pass_hidden = "HiddenSecretPassword2026!";
|
||||
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
|
||||
+21
-20
@@ -11,6 +11,7 @@ use sanctum::{
|
||||
crypto::{
|
||||
dek_to_mnemonic, derive_kek, encrypt_chunk, generate_dek, generate_salt, unwrap_dek,
|
||||
wrap_dek, KdfParams, CHUNK_SIZE, FORMAT_VERSION, FORMAT_VERSION_V1,
|
||||
MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||
},
|
||||
recovery::{export_header_backup, restore_header_backup, restore_header_from_recovery_key},
|
||||
storage::Database,
|
||||
@@ -34,8 +35,8 @@ async fn test_sanctum_full_container_lifecycle() {
|
||||
// 1. Initialisierung
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024, // Schnell für Tests
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).expect("KEK derivation");
|
||||
@@ -176,8 +177,8 @@ async fn test_sanctum_password_change() {
|
||||
// 1. Initialisierung mit Passwort v1
|
||||
let salt_v1 = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek_v1 = derive_kek(password_v1, &salt_v1, &kdf_params).expect("KEK v1");
|
||||
@@ -262,8 +263,8 @@ async fn test_sanctum_lz4_compression_efficiency() {
|
||||
let password = "CompressTestPassword2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).expect("KEK");
|
||||
@@ -333,8 +334,8 @@ async fn test_sanctum_v1_backward_compatibility() {
|
||||
let password = "V1LegacyPassword!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).expect("KEK");
|
||||
@@ -392,8 +393,8 @@ async fn test_sanctum_disaster_recovery_workflow() {
|
||||
let initial_password = "PrimaryPassword2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(initial_password, &salt, &kdf_params).expect("KEK");
|
||||
@@ -511,8 +512,8 @@ async fn test_anti_leak_and_inactivity_shield() {
|
||||
let password = "ShieldedContainerPass2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).expect("derive kek");
|
||||
@@ -625,8 +626,8 @@ async fn test_hidden_vault_and_storage_compaction_integration() {
|
||||
let salt0 = generate_salt();
|
||||
let salt1 = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
@@ -812,8 +813,8 @@ async fn test_plausible_deniability_phase1_indistinguishability_and_safeguards()
|
||||
let pass_decoy = "OuterDecoyPassphrase2026!";
|
||||
let pass_hidden = "InnerHiddenVaultPass2026!";
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
@@ -990,8 +991,8 @@ async fn test_sanctum_online_backup_and_restore() {
|
||||
let password = "BackupTestPassword2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
|
||||
@@ -1079,8 +1080,8 @@ async fn test_webdav_loopback_session_token_and_host_validation() {
|
||||
let password = "WebDavSecPassword2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
|
||||
|
||||
@@ -28,8 +28,8 @@ async fn test_live_crash_and_recovery_stress() {
|
||||
let password = "LiveStressPassword2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: sanctum::crypto::MIN_MEMORY_COST_KIB,
|
||||
time_cost: sanctum::crypto::MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).expect("KEK derivation");
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
use std::path::PathBuf;
|
||||
use sanctum::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, MIN_MEMORY_COST_KIB,
|
||||
MIN_TIME_COST,
|
||||
};
|
||||
use sanctum::mount::is_loopback_host;
|
||||
use sanctum::storage::Database;
|
||||
|
||||
#[test]
|
||||
fn test_is_loopback_host_comprehensive() {
|
||||
// Gültige IPv4 Loopback Varianten
|
||||
assert!(is_loopback_host("127.0.0.1"));
|
||||
assert!(is_loopback_host("127.0.0.1:80"));
|
||||
assert!(is_loopback_host("127.0.0.1:8080"));
|
||||
assert!(is_loopback_host("127.0.0.1:65535"));
|
||||
|
||||
// Gültige Hostname Loopback Varianten
|
||||
assert!(is_loopback_host("localhost"));
|
||||
assert!(is_loopback_host("localhost:80"));
|
||||
assert!(is_loopback_host("localhost:8443"));
|
||||
assert!(is_loopback_host("LOCALHOST"));
|
||||
assert!(is_loopback_host("LocalHost:9000"));
|
||||
|
||||
// Gültige IPv6 Loopback Varianten
|
||||
assert!(is_loopback_host("[::1]"));
|
||||
assert!(is_loopback_host("[::1]:80"));
|
||||
assert!(is_loopback_host("[::1]:8443"));
|
||||
assert!(is_loopback_host("::1"));
|
||||
|
||||
// DNS-Rebinding & Spoofing Angriffe (MÜSSEN abgewiesen werden)
|
||||
assert!(!is_loopback_host("127.0.0.1.attacker.com"));
|
||||
assert!(!is_loopback_host("localhost.attacker.com"));
|
||||
assert!(!is_loopback_host("localhost.evil.org:8080"));
|
||||
assert!(!is_loopback_host("notlocalhost"));
|
||||
assert!(!is_loopback_host("attacker.com"));
|
||||
assert!(!is_loopback_host("attacker.com:127001"));
|
||||
assert!(!is_loopback_host("192.168.1.1"));
|
||||
assert!(!is_loopback_host("10.0.0.1"));
|
||||
assert!(!is_loopback_host("0.0.0.0"));
|
||||
assert!(!is_loopback_host(""));
|
||||
assert!(!is_loopback_host(" "));
|
||||
assert!(!is_loopback_host("foo:bar:baz"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wal_and_shm_cleanup_on_close() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let container_path: PathBuf = temp_dir.join(format!("test_wal_cleanup_{}.sanctum", std::process::id()));
|
||||
if container_path.exists() {
|
||||
let _ = std::fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek("WalCleanupPassword2026!", &salt, &kdf_params).unwrap();
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||
|
||||
let db = Database::open(&container_path).unwrap();
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
||||
|
||||
// Erstelle Knoten, um Schreibaktivität im WAL zu erzeugen
|
||||
let _ = db.create_node(1, "test_file.txt", false).unwrap();
|
||||
db.checkpoint().unwrap();
|
||||
drop(db);
|
||||
|
||||
// Bereinigungslogik (wie in mount.rs unmount) ausführen
|
||||
let base_os = container_path.as_os_str().to_os_string();
|
||||
let mut wal = base_os.clone();
|
||||
wal.push("-wal");
|
||||
let _ = std::fs::remove_file(&wal);
|
||||
let mut shm = base_os;
|
||||
shm.push("-shm");
|
||||
let _ = std::fs::remove_file(&shm);
|
||||
|
||||
assert!(!PathBuf::from(wal).exists(), "WAL-Datei darf nach sauberem Unmount nicht zurückbleiben");
|
||||
assert!(!PathBuf::from(shm).exists(), "SHM-Datei darf nach sauberem Unmount nicht zurückbleiben");
|
||||
|
||||
let _ = std::fs::remove_file(&container_path);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use sanctum::sync::validate_node_name;
|
||||
|
||||
#[test]
|
||||
fn test_validate_node_name_rejections() {
|
||||
// 1. Leere Namen
|
||||
assert!(validate_node_name("").is_err());
|
||||
|
||||
// 2. Relative Verzeichnisreferenzen
|
||||
assert!(validate_node_name(".").is_err());
|
||||
assert!(validate_node_name("..").is_err());
|
||||
|
||||
// 3. Pfadtrenner
|
||||
assert!(validate_node_name("foo/bar").is_err());
|
||||
assert!(validate_node_name("foo\\bar").is_err());
|
||||
assert!(validate_node_name("../evil.exe").is_err());
|
||||
assert!(validate_node_name("..\\evil.exe").is_err());
|
||||
assert!(validate_node_name("/root_file").is_err());
|
||||
|
||||
// 4. Null-Bytes
|
||||
assert!(validate_node_name("test\0file.txt").is_err());
|
||||
|
||||
// 5. Steuerzeichen (< 0x20)
|
||||
assert!(validate_node_name("line\nbreak.txt").is_err());
|
||||
assert!(validate_node_name("carriage\rreturn.txt").is_err());
|
||||
assert!(validate_node_name("tab\tseparated.txt").is_err());
|
||||
assert!(validate_node_name("control\x01char.txt").is_err());
|
||||
|
||||
// 6. Windows reservierte Gerätenamen
|
||||
let reserved_names = [
|
||||
"CON", "con", "prn", "PRN", "aux", "AUX", "nul", "NUL",
|
||||
"COM1", "com2", "COM9", "lpt1", "LPT2", "LPT9",
|
||||
"con.txt", "aux.dat", "NUL.tar.gz", "com1.log", "prn.pdf",
|
||||
];
|
||||
for res in reserved_names {
|
||||
assert!(
|
||||
validate_node_name(res).is_err(),
|
||||
"Reservierter Name '{}' muss abgewiesen werden",
|
||||
res
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_node_name_accepted() {
|
||||
let valid_names = [
|
||||
"document.pdf",
|
||||
"my_notes_2026.txt",
|
||||
"archive.tar.gz",
|
||||
"spaces are allowed.docx",
|
||||
"unicode_äöü_ß_test.rs",
|
||||
"hyphens-and_underscores.json",
|
||||
"file (1).jpg",
|
||||
"normal_file",
|
||||
];
|
||||
for valid in valid_names {
|
||||
assert!(
|
||||
validate_node_name(valid).is_ok(),
|
||||
"Gültiger Name '{}' sollte akzeptiert werden",
|
||||
valid
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
|
||||
use sanctum::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_slot0_payload,
|
||||
wrap_slot1_payload, KdfParams, MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
use sanctum::sync::{delete_orphans_in_vault, SyncStats};
|
||||
|
||||
fn temp_db_path(prefix: &str) -> PathBuf {
|
||||
let mut path = std::env::temp_dir();
|
||||
let id: u64 = OsRng.next_u64();
|
||||
path.push(format!("sanctum_test_{}_{}.sanctum", prefix, id));
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_carrier_protection_in_storage_and_sync() {
|
||||
let path = temp_db_path("carrier_sec");
|
||||
let carrier_name = "secret_carrier.iso";
|
||||
let carrier_size_bytes = 2 * 1024 * 1024; // 2 MB
|
||||
|
||||
let pass_decoy = "DecoyPassword2026!";
|
||||
let pass_hidden = "SuperSecretHiddenPassword2026!";
|
||||
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
let salt_0 = generate_salt();
|
||||
let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap();
|
||||
let dek_0 = generate_dek();
|
||||
|
||||
let salt_1 = generate_salt();
|
||||
let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap();
|
||||
let dek_1 = generate_dek();
|
||||
|
||||
let carrier_node_id = 3i64;
|
||||
let (wrapped_0, nonce_0, tag_0) =
|
||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||
let (wrapped_1, nonce_1, tag_1) =
|
||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||
|
||||
let db = Database::open(&path).expect("Open database");
|
||||
|
||||
let created_cid = db
|
||||
.init_schema_with_carrier(
|
||||
&salt_0,
|
||||
&kdf_params,
|
||||
&wrapped_0,
|
||||
&nonce_0,
|
||||
&tag_0,
|
||||
Some((
|
||||
carrier_name,
|
||||
carrier_size_bytes,
|
||||
&salt_1,
|
||||
&kdf_params,
|
||||
&wrapped_1,
|
||||
&nonce_1,
|
||||
&tag_1,
|
||||
&dek_0,
|
||||
&dek_1,
|
||||
)),
|
||||
)
|
||||
.expect("Init carrier schema");
|
||||
|
||||
db.checkpoint().unwrap();
|
||||
assert_eq!(created_cid, Some(carrier_node_id));
|
||||
|
||||
// 1. Direkter Löschversuch der Trägerdatei muss fail-closed abgewehrt werden
|
||||
let del_res = db.delete_node(carrier_node_id);
|
||||
assert!(del_res.is_err(), "Löschen der Trägerdatei muss fehlschlagen");
|
||||
assert!(del_res.unwrap_err().to_string().contains("Carrier-Schutz"));
|
||||
|
||||
// 2. Umbenennung der Trägerdatei muss fail-closed abgewehrt werden
|
||||
let rename_res = db.rename_node_in_vault(carrier_node_id, 1, "renamed.iso", 0, &dek_0);
|
||||
assert!(rename_res.is_err(), "Umbenennen der Trägerdatei muss fehlschlagen");
|
||||
assert!(rename_res.unwrap_err().to_string().contains("Carrier-Schutz"));
|
||||
|
||||
// 3. Truncate der Trägerdatei muss fail-closed abgewehrt werden
|
||||
let trunc_res = db.truncate_chunks_after(carrier_node_id, 0);
|
||||
assert!(trunc_res.is_err(), "Truncate der Trägerdatei muss fehlschlagen");
|
||||
assert!(trunc_res.unwrap_err().to_string().contains("Carrier-Schutz"));
|
||||
|
||||
// 4. delete_orphans_in_vault mit leerem lokalem Pfad-Set:
|
||||
// Der Carrier darf unter keinen Umständen gelöscht werden!
|
||||
let local_paths = HashSet::new();
|
||||
let mut stats = SyncStats::default();
|
||||
let orphan_res = delete_orphans_in_vault(
|
||||
&db,
|
||||
0, // Decoy Vault ID
|
||||
&dek_0,
|
||||
1, // Root-Knoten
|
||||
"",
|
||||
&local_paths,
|
||||
false, // kein dry_run
|
||||
true, // quiet
|
||||
&mut stats,
|
||||
);
|
||||
assert!(orphan_res.is_ok(), "delete_orphans_in_vault muss ohne Fehler durchlaufen");
|
||||
assert_eq!(stats.files_deleted, 0, "Carrier-Datei darf nicht gelöscht worden sein");
|
||||
|
||||
// Sicherstellen, dass Carrier nach wie vor in der DB existiert
|
||||
let carrier_rec = db.get_node_by_id(carrier_node_id).unwrap();
|
||||
assert!(carrier_rec.is_some(), "Carrier-Knoten muss nach Sync unversehrt vorhanden sein");
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
+2
-2
@@ -15,8 +15,8 @@ fn create_test_container(path: &Path) -> (Database, [u8; 32]) {
|
||||
let db = Database::open(path).expect("Open database");
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: sanctum::crypto::MIN_MEMORY_COST_KIB,
|
||||
time_cost: sanctum::crypto::MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek("sync_password", &salt, &kdf_params).unwrap();
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
use std::path::PathBuf;
|
||||
use sanctum::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, MIN_MEMORY_COST_KIB,
|
||||
MIN_TIME_COST,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
|
||||
#[test]
|
||||
fn test_reject_arbitrary_sqlite_database() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let db_path: PathBuf = temp_dir.join(format!("test_foreign_sqlite_{}.db", std::process::id()));
|
||||
if db_path.exists() {
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
// Erstelle eine fremde SQLite-Datenbank mit beliebigen Daten
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||
conn.execute(
|
||||
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute("INSERT INTO users (name) VALUES ('Alice')", []).unwrap();
|
||||
}
|
||||
|
||||
// Sanctum Database::open muss die Datei sofort fail-closed ablehnen
|
||||
let res = Database::open(&db_path);
|
||||
match res {
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("kein gültiger Sanctum-Container"),
|
||||
"Fehler muss ungültigen Container abweisen: {msg}"
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("Fremde SQLite-DB ohne meta-Tabelle muss abgelehnt werden"),
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reject_sqlite_database_with_invalid_magic() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let db_path: PathBuf = temp_dir.join(format!("test_invalid_magic_{}.db", std::process::id()));
|
||||
if db_path.exists() {
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
// Erstelle SQLite DB mit 'meta' Tabelle, aber falscher Magic
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||
conn.execute(
|
||||
"CREATE TABLE meta (
|
||||
slot_id INTEGER PRIMARY KEY,
|
||||
magic BLOB NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
kdf_salt BLOB NOT NULL,
|
||||
kdf_params TEXT NOT NULL,
|
||||
wrapped_dek BLOB NOT NULL,
|
||||
header_nonce BLOB NOT NULL,
|
||||
header_tag BLOB NOT NULL
|
||||
);",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag)
|
||||
VALUES (0, ?1, 2, ?2, '{}', ?3, ?4, ?5)",
|
||||
rusqlite::params![
|
||||
b"EVIL_MAGIC",
|
||||
&[0u8; 16][..],
|
||||
&[0u8; 40][..],
|
||||
&[0u8; 12][..],
|
||||
&[0u8; 16][..]
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let res = Database::open(&db_path);
|
||||
match res {
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("kein gültiger Sanctum-Container"),
|
||||
"Fehler muss ungültigen Magic-Header monieren: {msg}"
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("Container mit ungültigen Magic Bytes muss abgelehnt werden"),
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reject_out_of_bounds_kdf_params() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let db_path: PathBuf = temp_dir.join(format!("test_oob_kdf_{}.sanctum", std::process::id()));
|
||||
if db_path.exists() {
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
// 1. Initialisiere gültigen Container
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek("ValidPassword2026!", &salt, &kdf_params).unwrap();
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||
|
||||
let db = Database::open(&db_path).unwrap();
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
||||
db.checkpoint().unwrap();
|
||||
drop(db);
|
||||
|
||||
// 2. Manipuliere KDF-Parameter in der meta-Tabelle auf gefährliche Werte (DoS: 16 GiB Memory)
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||
conn.execute(
|
||||
"UPDATE meta SET kdf_params = '{\"memory_cost\": 16777216, \"time_cost\": 3, \"parallelism\": 4}' WHERE slot_id = 0",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// 3. Öffnen und read_slots() aufrufen: muss mit Kdf-Param-Fehler abbrechen
|
||||
let db_reopened = Database::open(&db_path).unwrap();
|
||||
let slots_res = db_reopened.read_slots();
|
||||
match slots_res {
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("KDF-Parameter"),
|
||||
"Fehler muss KDF-Parameter monieren: {msg}"
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("Überhöhte KDF-Parameter müssen fail-closed abgewehrt werden"),
|
||||
}
|
||||
|
||||
// 4. Manipuliere KDF-Parameter auf zu schwache Werte (< MIN_MEMORY_COST_KIB)
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||
conn.execute(
|
||||
"UPDATE meta SET kdf_params = '{\"memory_cost\": 1024, \"time_cost\": 3, \"parallelism\": 4}' WHERE slot_id = 0",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let slots_res2 = db_reopened.read_slots();
|
||||
match slots_res2 {
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("KDF-Parameter"),
|
||||
"Fehler muss KDF-Parameter monieren: {msg}"
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("Zu schwache KDF-Parameter müssen fail-closed abgewehrt werden"),
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reject_corrupted_slot_lengths() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let db_path: PathBuf = temp_dir.join(format!("test_slot_lengths_{}.sanctum", std::process::id()));
|
||||
if db_path.exists() {
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek("ValidPassword2026!", &salt, &kdf_params).unwrap();
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||
|
||||
let db = Database::open(&db_path).unwrap();
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
||||
db.checkpoint().unwrap();
|
||||
drop(db);
|
||||
|
||||
// Manipuliere Salt-Länge auf 8 Byte statt 16 Byte
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||
conn.execute(
|
||||
"UPDATE meta SET kdf_salt = ?1 WHERE slot_id = 0",
|
||||
[&[0u8; 8][..]],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let db_reopened = Database::open(&db_path).unwrap();
|
||||
let res = db_reopened.read_slots();
|
||||
match res {
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("Salt-Länge"),
|
||||
"Fehler muss fehlerhafte Salt-Länge monieren: {msg}"
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("Ungültige Salt-Länge muss abgewiesen werden"),
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use sanctum::upgrade::{
|
||||
run_upgrade, verify_minisign_signature, UpgradeOptions, SANCTUM_RELEASE_PUBKEY,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_minisign_signature_valid() {
|
||||
let payload = b"Hello Sanctum Release 0.7.0 Security Testing\n";
|
||||
let sig_text = "\
|
||||
untrusted comment: signature from minisign secret key\n\
|
||||
RUSsVphPgr8155LCucDq8RzDmWMzWElzhBmHrS5p+qJlOCGU4AQpn2F28MVPFrnoGPYEHx7/6TlKg2b45DlYuBQ0+ASeLIzMLwc=\n\
|
||||
trusted comment: timestamp:1789750897\tfile:test_payload.txt\thashed\n\
|
||||
0l7KsNJkDTo2uOKL8UAiaJOWuhwhGTVZ5fn0JFMdxXE9riYixQO9YnBpSVqz8iCDhWrz8hJBnmUrIWnaRaXVCA==\n";
|
||||
|
||||
let res = verify_minisign_signature(payload, sig_text, SANCTUM_RELEASE_PUBKEY);
|
||||
assert!(res.is_ok(), "Gültige Minisign-Signatur muss erfolgreich verifiziert werden: {:?}", res.err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minisign_signature_tampered_payload() {
|
||||
let tampered_payload = b"Hello Sanctum Release 0.7.0 Tampered Payload!\n";
|
||||
let sig_text = "\
|
||||
untrusted comment: signature from minisign secret key\n\
|
||||
RUSsVphPgr8155LCucDq8RzDmWMzWElzhBmHrS5p+qJlOCGU4AQpn2F28MVPFrnoGPYEHx7/6TlKg2b45DlYuBQ0+ASeLIzMLwc=\n\
|
||||
trusted comment: timestamp:1789750897\tfile:test_payload.txt\thashed\n\
|
||||
0l7KsNJkDTo2uOKL8UAiaJOWuhwhGTVZ5fn0JFMdxXE9riYixQO9YnBpSVqz8iCDhWrz8hJBnmUrIWnaRaXVCA==\n";
|
||||
|
||||
let res = verify_minisign_signature(tampered_payload, sig_text, SANCTUM_RELEASE_PUBKEY);
|
||||
assert!(res.is_err(), "Manipulierte Binärdaten müssen die Minisign-Prüfung verfehlen");
|
||||
assert!(res.unwrap_err().to_string().contains("FEHLGESCHLAGEN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minisign_signature_wrong_key() {
|
||||
let payload = b"Hello Sanctum Release 0.7.0 Security Testing\n";
|
||||
let sig_text = "\
|
||||
untrusted comment: signature from minisign secret key\n\
|
||||
RUSsVphPgr8155LCucDq8RzDmWMzWElzhBmHrS5p+qJlOCGU4AQpn2F28MVPFrnoGPYEHx7/6TlKg2b45DlYuBQ0+ASeLIzMLwc=\n\
|
||||
trusted comment: timestamp:1789750897\tfile:test_payload.txt\thashed\n\
|
||||
0l7KsNJkDTo2uOKL8UAiaJOWuhwhGTVZ5fn0JFMdxXE9riYixQO9YnBpSVqz8iCDhWrz8hJBnmUrIWnaRaXVCA==\n";
|
||||
|
||||
// Ein anderer, unautorisierter Public Key
|
||||
let other_key = "RWSb0a70vF1X8qIjc7xi28Gmw+cIWbMipOy4L6ToJEUPkWDw3c0qIjc7";
|
||||
let res = verify_minisign_signature(payload, sig_text, other_key);
|
||||
assert!(res.is_err(), "Signaturprüfung mit fremdem Schlüssel muss fehlschlagen");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upgrade_blocks_unofficial_url_without_insecure_flag() {
|
||||
let options = UpgradeOptions {
|
||||
check_only: true,
|
||||
yes: false,
|
||||
force: false,
|
||||
base_url: "https://evil-attacker.org/sanctum".into(),
|
||||
insecure_url: false,
|
||||
};
|
||||
|
||||
let res = run_upgrade(&options);
|
||||
assert!(res.is_err(), "Inoffizielle Server-URL ohne --insecure-url muss blockiert werden");
|
||||
let err_msg = res.unwrap_err().to_string();
|
||||
assert!(
|
||||
err_msg.contains("Sicherheitsverstoß") || err_msg.contains("nicht vertrauenswürdig"),
|
||||
"Fehler muss Sicherheitsverstoß anzeigen: {err_msg}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user