fix(crypto): K-01 — format v3 canonical metadata authentication

This commit is contained in:
2026-09-19 00:26:27 +02:00
parent 151fdba09d
commit ad531d8393
7 changed files with 609 additions and 12 deletions
+194 -2
View File
@@ -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);
}