fix(upgrade): U-02 — validate version in minisign trusted comment against release tag
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+48
-3
@@ -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<String> {
|
||||
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")
|
||||
);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user