release: v0.7.2 — Security Audit Remediation (SA-01 bis SA-07)
- SA-01: Container-DoS / KDF-Amplification Schutz mit Pre-KDF Validierung, max 2 Slots (nur 0 und 1), Slot 0 Pflicht und strikten BLOB-Laengen - SA-02: Release-Signierung in CI entkoppelt (getrennte build und sign-and-release Jobs, Secret-Isolation) - SA-03: Pinned Download-Integritaet fuer minisign.exe in CI via SHA-256 - SA-04: Immutable Action-Pinning (@sha) und Toolchain-Pinning (1.85.0) in CI - SA-05: Session-Token vollstaendig aus URIs verbannt (403 Forbidden bei Vorkommen im Pfad/Query) - SA-06: Constant-Time Token- und Auth-Vergleiche via subtle::ConstantTimeEq - SA-07: Dokumentations-Klarstellung bzgl. logischem Shredding vs. physischer SSD/FTL/CoW-Persistenz
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sanctum::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_slot0_payload,
|
||||
wrap_slot1_payload, KdfParams, MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||
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};
|
||||
@@ -41,8 +41,7 @@ fn test_carrier_protection_in_storage_and_sync() {
|
||||
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_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();
|
||||
|
||||
@@ -74,18 +73,33 @@ fn test_carrier_protection_in_storage_and_sync() {
|
||||
|
||||
// 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.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"));
|
||||
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"));
|
||||
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!
|
||||
@@ -102,16 +116,26 @@ fn test_carrier_protection_in_storage_and_sync() {
|
||||
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");
|
||||
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");
|
||||
assert!(
|
||||
carrier_rec.is_some(),
|
||||
"Carrier-Knoten muss nach Sync unversehrt vorhanden sein"
|
||||
);
|
||||
|
||||
// 5. R-02: sync_single_file_to_host muss Carrier-Pull strikt ablehnen
|
||||
let carrier_node = db.get_node_by_id(carrier_node_id).unwrap().unwrap();
|
||||
let local_dest_file = std::env::temp_dir().join(format!("carrier_leak_{}.iso", std::process::id()));
|
||||
let local_dest_file =
|
||||
std::env::temp_dir().join(format!("carrier_leak_{}.iso", std::process::id()));
|
||||
let pull_single_res = sanctum::sync::sync_single_file_to_host(
|
||||
&db,
|
||||
0,
|
||||
@@ -122,11 +146,18 @@ fn test_carrier_protection_in_storage_and_sync() {
|
||||
false,
|
||||
false,
|
||||
);
|
||||
assert!(pull_single_res.is_err(), "sync_single_file_to_host auf Carrier muss fehlschlagen");
|
||||
assert!(!local_dest_file.exists(), "Trägerdatei darf niemals auf den Host geschrieben werden");
|
||||
assert!(
|
||||
pull_single_res.is_err(),
|
||||
"sync_single_file_to_host auf Carrier muss fehlschlagen"
|
||||
);
|
||||
assert!(
|
||||
!local_dest_file.exists(),
|
||||
"Trägerdatei darf niemals auf den Host geschrieben werden"
|
||||
);
|
||||
|
||||
// 6. R-02: run_sync mit Pull muss die Trägerdatei überspringen
|
||||
let local_pull_dir = std::env::temp_dir().join(format!("sanctum_pull_test_{}", std::process::id()));
|
||||
let local_pull_dir =
|
||||
std::env::temp_dir().join(format!("sanctum_pull_test_{}", std::process::id()));
|
||||
let sync_options = sanctum::sync::SyncOptions {
|
||||
direction: sanctum::sync::SyncDirection::Pull,
|
||||
delete: false,
|
||||
@@ -144,9 +175,15 @@ fn test_carrier_protection_in_storage_and_sync() {
|
||||
&local_pull_dir.to_string_lossy(),
|
||||
&sync_options,
|
||||
);
|
||||
assert!(pull_res.is_ok(), "run_sync pull muss erfolgreich durchlaufen");
|
||||
assert!(
|
||||
pull_res.is_ok(),
|
||||
"run_sync pull muss erfolgreich durchlaufen"
|
||||
);
|
||||
let pulled_carrier = local_pull_dir.join(carrier_name);
|
||||
assert!(!pulled_carrier.exists(), "Trägerdatei darf bei recursive pull nicht auf den Host kopiert werden");
|
||||
assert!(
|
||||
!pulled_carrier.exists(),
|
||||
"Trägerdatei darf bei recursive pull nicht auf den Host kopiert werden"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&local_pull_dir);
|
||||
|
||||
// 7. R-02: VFS Copy Schutz
|
||||
@@ -166,14 +203,22 @@ fn test_carrier_protection_in_storage_and_sync() {
|
||||
let to_copy = DavPath::new("/carrier_copy.iso").unwrap();
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let copy_src_res = rt.block_on(fs.copy(&from_carrier, &to_copy));
|
||||
assert!(copy_src_res.is_err(), "SanctumFs::copy mit Carrier als Quelle muss FsError::Forbidden liefern");
|
||||
assert!(
|
||||
copy_src_res.is_err(),
|
||||
"SanctumFs::copy mit Carrier als Quelle muss FsError::Forbidden liefern"
|
||||
);
|
||||
|
||||
// Copy Ziel = Carrier
|
||||
let _regular_file = db.create_node_in_vault(0, 1, "regular.txt", false, &dek_0).unwrap();
|
||||
let _regular_file = db
|
||||
.create_node_in_vault(0, 1, "regular.txt", false, &dek_0)
|
||||
.unwrap();
|
||||
let from_regular = DavPath::new("/regular.txt").unwrap();
|
||||
let to_carrier = DavPath::new(&format!("/{}", carrier_name)).unwrap();
|
||||
let copy_dst_res = rt.block_on(fs.copy(&from_regular, &to_carrier));
|
||||
assert!(copy_dst_res.is_err(), "SanctumFs::copy mit Carrier als Ziel muss FsError::Forbidden liefern");
|
||||
assert!(
|
||||
copy_dst_res.is_err(),
|
||||
"SanctumFs::copy mit Carrier als Ziel muss FsError::Forbidden liefern"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user