fix(crypto): K-01 — format v3 canonical metadata authentication
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
use sanctum::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, MIN_MEMORY_COST_KIB,
|
||||
MIN_TIME_COST,
|
||||
derive_kek, generate_dek, generate_salt, wrap_dek, wrap_slot0_payload, KdfParams,
|
||||
MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
use sanctum::verify::verify_container;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
@@ -677,3 +678,194 @@ fn test_container_meta_authenticate_defense_in_depth() {
|
||||
};
|
||||
assert!(meta_valid_1.authenticate("TestPassword2026!").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_k01_metadata_tampering_detected_by_verify() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let db_path: PathBuf = temp_dir.join(format!("test_k01_meta_{}.sanctum", std::process::id()));
|
||||
if db_path.exists() {
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
let password = "SecretMasterPassword2026!";
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let salt = generate_salt();
|
||||
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, nonce, tag) = wrap_slot0_payload(&kek, &dek, 0).unwrap();
|
||||
|
||||
let db = Database::open(&db_path).unwrap();
|
||||
db.set_active_dek(dek.clone());
|
||||
db.init_schema_with_carrier(&salt, &kdf_params, &wrapped_dek, &nonce, &tag, None)
|
||||
.unwrap();
|
||||
|
||||
// Knoten anlegen
|
||||
let node = db
|
||||
.create_node(1, "secret_financials.xlsx", false)
|
||||
.unwrap();
|
||||
db.update_node_size_and_time(node.id, 50000, 1000).unwrap();
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
// 1. Initialer Zustand: Verifikation muss erfolgreich sein
|
||||
let report = verify_container(&db_path, Some(&dek), true).unwrap();
|
||||
assert!(
|
||||
report.is_healthy(),
|
||||
"Initialer V3-Container muss gesund sein, aber Fehler: {:?}",
|
||||
report.errors
|
||||
);
|
||||
|
||||
// 2. Angreifer manipuliert Dateigröße direkt in SQLite (ohne MAC aktualisieren zu können)
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||
conn.execute(
|
||||
"UPDATE nodes SET size = 999999 WHERE name = 'secret_financials.xlsx';",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Verifikation MUSS fehlschlagen und den K-01 Metadatenfehler melden!
|
||||
let tampered_report = verify_container(&db_path, Some(&dek), true).unwrap();
|
||||
assert!(
|
||||
!tampered_report.is_healthy(),
|
||||
"Container mit manipulierter Knotengröße darf nicht als gesund eingestuft werden!"
|
||||
);
|
||||
assert!(
|
||||
tampered_report
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| e.contains("K-01") || e.contains("Metadaten-MAC")),
|
||||
"Fehlermeldung zu Metadaten-MAC erwartet, erhalten: {:?}",
|
||||
tampered_report.errors
|
||||
);
|
||||
|
||||
// 3. Angreifer manipuliert Dateinamen direkt in SQLite
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||
conn.execute(
|
||||
"UPDATE nodes SET name = 'backdoor.exe' WHERE id = ?1;",
|
||||
[node.id],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let tampered_name_report = verify_container(&db_path, Some(&dek), true).unwrap();
|
||||
assert!(
|
||||
!tampered_name_report.is_healthy(),
|
||||
"Container mit manipuliertem Knotennamen darf nicht als gesund eingestuft werden!"
|
||||
);
|
||||
assert!(
|
||||
tampered_name_report
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| e.contains("K-01") || e.contains("Metadaten-MAC")),
|
||||
"Fehlermeldung zu Metadaten-MAC erwartet, erhalten: {:?}",
|
||||
tampered_name_report.errors
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_k01_upgrade_format_v2_to_v3() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let db_path: PathBuf = temp_dir.join(format!("test_k01_upgrade_{}.sanctum", std::process::id()));
|
||||
if db_path.exists() {
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
let password = "UpgradeMasterPassword2026!";
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let salt = generate_salt();
|
||||
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, nonce, tag) = wrap_slot0_payload(&kek, &dek, 0).unwrap();
|
||||
|
||||
// Erzeuge bewusst einen V2-Container ohne metadata_mac
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE meta (
|
||||
slot_id INTEGER NOT NULL 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
|
||||
);
|
||||
CREATE TABLE nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
parent_id INTEGER,
|
||||
name TEXT NOT NULL,
|
||||
is_dir INTEGER NOT NULL,
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
modified_at INTEGER NOT NULL,
|
||||
is_carrier INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE chunks (
|
||||
node_id INTEGER NOT NULL,
|
||||
chunk_index INTEGER NOT NULL,
|
||||
nonce BLOB NOT NULL,
|
||||
tag BLOB NOT NULL,
|
||||
ciphertext BLOB NOT NULL,
|
||||
PRIMARY KEY (node_id, chunk_index)
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let params_json = serde_json::to_string(&kdf_params).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, ?6)",
|
||||
rusqlite::params![
|
||||
sanctum::crypto::MAGIC_BYTES.as_slice(),
|
||||
salt.as_slice(),
|
||||
params_json,
|
||||
wrapped_dek,
|
||||
nonce.as_slice(),
|
||||
tag.as_slice(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO nodes (id, parent_id, name, is_dir, size, created_at, modified_at)
|
||||
VALUES (1, NULL, '', 1, 0, 100, 100), (2, NULL, '', 1, 0, 100, 100);",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let db = Database::open(&db_path).unwrap();
|
||||
let meta_before = db.read_meta().unwrap();
|
||||
assert_eq!(meta_before.version, 2);
|
||||
|
||||
// Upgrade auf Format V3 durchführen
|
||||
db.upgrade_to_v3(&dek).unwrap();
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
let meta_after = db.read_meta().unwrap();
|
||||
assert_eq!(meta_after.version, 3);
|
||||
|
||||
// V3-Container muss nach dem Upgrade integer und gesund sein
|
||||
let report = verify_container(&db_path, Some(&dek), true).unwrap();
|
||||
assert!(
|
||||
report.is_healthy(),
|
||||
"Container nach upgrade_to_v3 muss gesund sein: {:?}",
|
||||
report.errors
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user