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

This commit is contained in:
2026-09-18 19:12:43 +02:00
parent 80a3bd1911
commit 436790abf0
29 changed files with 1553 additions and 277 deletions
+7 -7
View File
@@ -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
View File
@@ -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();
+2 -2
View File
@@ -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");
+84
View File
@@ -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);
}
+62
View File
@@ -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
);
}
}
+113
View File
@@ -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
View File
@@ -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();
+216
View File
@@ -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);
}
+64
View File
@@ -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}"
);
}