diff --git a/scripts/package-release-linux.ps1 b/scripts/package-release-linux.ps1 index e05a5f8..422ee7f 100644 --- a/scripts/package-release-linux.ps1 +++ b/scripts/package-release-linux.ps1 @@ -115,7 +115,7 @@ $SigFile = Join-Path $DistDir "SHA256SUMS.txt.minisig" if (Test-Path $KeyFile) { if (Test-Path $MinisignExe) { if (Test-Path $SigFile) { Remove-Item $SigFile -Force } - & $MinisignExe -S -s $KeyFile -m $ChecksumFile -W -x $SigFile + & $MinisignExe -S -s $KeyFile -m $ChecksumFile -W -x $SigFile -t "version:$Version" if ($LASTEXITCODE -eq 0 -and (Test-Path $SigFile)) { Write-Host "[OK] Minisign-Signatur aktualisiert: dist\SHA256SUMS.txt.minisig" -ForegroundColor Green } else { diff --git a/scripts/package-release.ps1 b/scripts/package-release.ps1 index af0dc2b..6d927b7 100644 --- a/scripts/package-release.ps1 +++ b/scripts/package-release.ps1 @@ -110,7 +110,7 @@ $SigFile = Join-Path $DistDir "SHA256SUMS.txt.minisig" if (Test-Path $KeyFile) { if (Test-Path $MinisignExe) { if (Test-Path $SigFile) { Remove-Item $SigFile -Force } - & $MinisignExe -S -s $KeyFile -m $ChecksumFile -W -x $SigFile + & $MinisignExe -S -s $KeyFile -m $ChecksumFile -W -x $SigFile -t "version:$Version" if ($LASTEXITCODE -eq 0 -and (Test-Path $SigFile)) { Write-Host "[OK] Minisign-Signatur erstellt: dist\SHA256SUMS.txt.minisig" -ForegroundColor Green } else { diff --git a/src/upgrade.rs b/src/upgrade.rs index 7808c27..0260ec9 100644 --- a/src/upgrade.rs +++ b/src/upgrade.rs @@ -67,7 +67,8 @@ impl Default for UpgradeOptions { /// Überprüft eine Minisign-Signatur für gegebene Binärdaten mit dem angegebenen Minisign Public Key (U-01). /// /// Erzwingt strikt Pre-Hashing (`allow_legacy = false`), um Malleability- und Längenangriffe auszuschließen. -pub fn verify_minisign_signature(data: &[u8], sig_str: &str, pubkey_str: &str) -> Result<()> { +/// Gibt bei erfolgreicher Verifikation den kryptografisch authentifizierten `trusted comment` zurück. +pub fn verify_minisign_signature(data: &[u8], sig_str: &str, pubkey_str: &str) -> Result { let pubkey = minisign_verify::PublicKey::from_base64(pubkey_str) .map_err(|e| anyhow::anyhow!("Ungültiger öffentlicher Minisign-Schlüssel: {e}"))?; let signature = minisign_verify::Signature::decode(sig_str) @@ -75,6 +76,49 @@ pub fn verify_minisign_signature(data: &[u8], sig_str: &str, pubkey_str: &str) - pubkey .verify(data, &signature, false) .map_err(|e| anyhow::anyhow!("Minisign-Signaturprüfung FEHLGESCHLAGEN: {e}. Das Release-Manifest ist nicht vertrauenswürdig!"))?; + Ok(signature.trusted_comment().to_string()) +} + +/// Extrahiert die Versionsnummer aus dem Trusted Comment einer Minisign-Signatur und +/// validiert sie gegen das erwartete Release-Tag (U-02). +pub fn validate_trusted_comment_version( + trusted_comment: &str, + expected_release: &str, +) -> Result<()> { + let clean_expected = expected_release + .trim() + .trim_start_matches(|c| c == 'v' || c == 'V'); + + if clean_expected.is_empty() { + bail!("Erwartetes Release-Tag darf nicht leer sein."); + } + + // Durchsuche die Tokens des Trusted Comments (getrennt durch Tabs oder Leerzeichen) + let found = trusted_comment.split_whitespace().any(|token| { + let val = if let Some(stripped) = token.strip_prefix("version:") { + stripped + } else if let Some(stripped) = token.strip_prefix("release:") { + stripped + } else if let Some(stripped) = token.strip_prefix("tag:") { + stripped + } else { + token + }; + + let clean_val = val.trim_start_matches(|c| c == 'v' || c == 'V'); + clean_val == clean_expected + }); + + if !found { + bail!( + "Signatur-Validierungsfehler (U-02): Der kryptografisch gesicherte Trusted Comment \ + '{}' stimmt nicht mit der erwarteten Release-Version '{}' überein! \ + Möglicher Replay-Angriff mit einer älteren signierten Prüfsummendatei.", + trusted_comment.trim(), + expected_release + ); + } + Ok(()) } @@ -501,10 +545,11 @@ pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> { " {} Verifiziere Minisign-Signatur gegen eingebetteten Herstellerschlüssel ...", ui::dim("[-]") ); - verify_minisign_signature(sums_content.as_bytes(), &sig_content, SANCTUM_RELEASE_PUBKEY) + let trusted_comment = verify_minisign_signature(sums_content.as_bytes(), &sig_content, SANCTUM_RELEASE_PUBKEY) .context("Minisign-Signaturprüfung für Prüfsummendatei FEHLGESCHLAGEN! Release ist nicht vertrauenswürdig.")?; + validate_trusted_comment_version(&trusted_comment, remote_tag)?; println!( - " • Signatur: {} (Minisign Ed25519)", + " • Signatur: {} (Minisign Ed25519, Version validiert)", ui::green("Gültig") ); diff --git a/tests/upgrade_security_test.rs b/tests/upgrade_security_test.rs index e67578b..141d5ba 100644 --- a/tests/upgrade_security_test.rs +++ b/tests/upgrade_security_test.rs @@ -1,5 +1,6 @@ use sanctum::upgrade::{ - run_upgrade, verify_minisign_signature, UpgradeOptions, SANCTUM_RELEASE_PUBKEY, + run_upgrade, validate_trusted_comment_version, verify_minisign_signature, UpgradeOptions, + SANCTUM_RELEASE_PUBKEY, }; #[test] @@ -108,3 +109,38 @@ trusted comment: timestamp:1789750897\tfile:test_payload.txt\thashed\n\ ); } +#[test] +fn test_u02_validate_trusted_comment_version_matching() { + // Standard Minisign format with -t "version:0.8.0" + let tc1 = "timestamp:1789750897\tversion:0.8.0\tfile:SHA256SUMS.txt\thashed"; + assert!(validate_trusted_comment_version(tc1, "0.8.0").is_ok()); + assert!(validate_trusted_comment_version(tc1, "v0.8.0").is_ok()); + + // Alternative tags like tag:v0.8.0 or release:0.8.0 + let tc2 = "release:0.8.0"; + assert!(validate_trusted_comment_version(tc2, "0.8.0").is_ok()); + let tc3 = "tag:v0.8.0"; + assert!(validate_trusted_comment_version(tc3, "0.8.0").is_ok()); +} + +#[test] +fn test_u02_reject_mismatched_release_tag_replay() { + // An attacker replays a valid signature from v0.7.0 for an update claiming to be v0.8.0 + let tc_old = "timestamp:1789750897\tversion:0.7.0\tfile:SHA256SUMS.txt\thashed"; + let res = validate_trusted_comment_version(tc_old, "v0.8.0"); + assert!( + res.is_err(), + "U-02: Ältere oder abweichende Version im Trusted Comment muss abgewiesen werden" + ); + let err_msg = res.unwrap_err().to_string(); + assert!( + err_msg.contains("U-02") && err_msg.contains("Replay-Angriff"), + "Fehlermeldung muss auf Replay-Angriff und U-02 hinweisen: {err_msg}" + ); + + // Empty or completely missing version token + let tc_no_ver = "timestamp:1789750897\tfile:SHA256SUMS.txt\thashed"; + let res2 = validate_trusted_comment_version(tc_no_ver, "0.8.0"); + assert!(res2.is_err()); +} +