fix(upgrade): U-02 — validate version in minisign trusted comment against release tag

This commit is contained in:
2026-09-19 09:43:39 +02:00
parent 78a9c233f8
commit 8dd54494b0
4 changed files with 87 additions and 6 deletions
+1 -1
View File
@@ -115,7 +115,7 @@ $SigFile = Join-Path $DistDir "SHA256SUMS.txt.minisig"
if (Test-Path $KeyFile) { if (Test-Path $KeyFile) {
if (Test-Path $MinisignExe) { if (Test-Path $MinisignExe) {
if (Test-Path $SigFile) { Remove-Item $SigFile -Force } 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)) { if ($LASTEXITCODE -eq 0 -and (Test-Path $SigFile)) {
Write-Host "[OK] Minisign-Signatur aktualisiert: dist\SHA256SUMS.txt.minisig" -ForegroundColor Green Write-Host "[OK] Minisign-Signatur aktualisiert: dist\SHA256SUMS.txt.minisig" -ForegroundColor Green
} else { } else {
+1 -1
View File
@@ -110,7 +110,7 @@ $SigFile = Join-Path $DistDir "SHA256SUMS.txt.minisig"
if (Test-Path $KeyFile) { if (Test-Path $KeyFile) {
if (Test-Path $MinisignExe) { if (Test-Path $MinisignExe) {
if (Test-Path $SigFile) { Remove-Item $SigFile -Force } 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)) { if ($LASTEXITCODE -eq 0 -and (Test-Path $SigFile)) {
Write-Host "[OK] Minisign-Signatur erstellt: dist\SHA256SUMS.txt.minisig" -ForegroundColor Green Write-Host "[OK] Minisign-Signatur erstellt: dist\SHA256SUMS.txt.minisig" -ForegroundColor Green
} else { } else {
+48 -3
View File
@@ -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). /// Ü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. /// 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<String> {
let pubkey = minisign_verify::PublicKey::from_base64(pubkey_str) let pubkey = minisign_verify::PublicKey::from_base64(pubkey_str)
.map_err(|e| anyhow::anyhow!("Ungültiger öffentlicher Minisign-Schlüssel: {e}"))?; .map_err(|e| anyhow::anyhow!("Ungültiger öffentlicher Minisign-Schlüssel: {e}"))?;
let signature = minisign_verify::Signature::decode(sig_str) 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 pubkey
.verify(data, &signature, false) .verify(data, &signature, false)
.map_err(|e| anyhow::anyhow!("Minisign-Signaturprüfung FEHLGESCHLAGEN: {e}. Das Release-Manifest ist nicht vertrauenswürdig!"))?; .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(()) Ok(())
} }
@@ -501,10 +545,11 @@ pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> {
" {} Verifiziere Minisign-Signatur gegen eingebetteten Herstellerschlüssel ...", " {} Verifiziere Minisign-Signatur gegen eingebetteten Herstellerschlüssel ...",
ui::dim("[-]") 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.")?; .context("Minisign-Signaturprüfung für Prüfsummendatei FEHLGESCHLAGEN! Release ist nicht vertrauenswürdig.")?;
validate_trusted_comment_version(&trusted_comment, remote_tag)?;
println!( println!(
" • Signatur: {} (Minisign Ed25519)", " • Signatur: {} (Minisign Ed25519, Version validiert)",
ui::green("Gültig") ui::green("Gültig")
); );
+37 -1
View File
@@ -1,5 +1,6 @@
use sanctum::upgrade::{ 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] #[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());
}