feat(updater): Pflicht-Signaturprüfung der Releases mit Minisign
- SHA256SUMS.txt muss mit dem eingebauten Minisign-Schlüssel (UPDATE_SIGNING_PUBLIC_KEY) signiert sein; fehlende oder ungültige Signaturen und Versionen ohne Schlüssel lehnen Updates ab - Prüfsummen und Signatur werden vor dem Paket geladen und geprüft - build-release.sh signiert mit MINISIGN_SECRET_KEY und prüft, dass der Schlüssel zum eingebauten öffentlichen Schlüssel passt; in der CI ist die Signatur Pflicht - publish-gitea-release.sh lädt SHA256SUMS.txt.minisig hoch - README: Einrichtung des Schlüsselpaars dokumentiert Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ jobs:
|
|||||||
squashfs-tools \
|
squashfs-tools \
|
||||||
dpkg-dev \
|
dpkg-dev \
|
||||||
curl \
|
curl \
|
||||||
|
minisign \
|
||||||
python3
|
python3
|
||||||
|
|
||||||
- name: Setup Rust Toolchain
|
- name: Setup Rust Toolchain
|
||||||
@@ -52,10 +53,13 @@ jobs:
|
|||||||
cargo clippy --all-targets -- -D warnings
|
cargo clippy --all-targets -- -D warnings
|
||||||
cargo test
|
cargo test
|
||||||
|
|
||||||
- name: Build Release Artifacts (.deb, .AppImage, SHA256SUMS.txt)
|
- name: Build Release Artifacts (.deb, .AppImage, SHA256SUMS.txt + .minisig)
|
||||||
run: |
|
run: |
|
||||||
chmod +x scripts/*.sh
|
chmod +x scripts/*.sh
|
||||||
./scripts/build-release.sh
|
./scripts/build-release.sh
|
||||||
|
env:
|
||||||
|
MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }}
|
||||||
|
INK_NOTES_REQUIRE_SIGNATURE: "1"
|
||||||
|
|
||||||
- name: Publish Release to Gitea
|
- name: Publish Release to Gitea
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
Generated
+7
@@ -1045,6 +1045,7 @@ dependencies = [
|
|||||||
"egui-wgpu",
|
"egui-wgpu",
|
||||||
"egui-winit",
|
"egui-winit",
|
||||||
"glam",
|
"glam",
|
||||||
|
"minisign-verify",
|
||||||
"octotablet",
|
"octotablet",
|
||||||
"pollster",
|
"pollster",
|
||||||
"printpdf",
|
"printpdf",
|
||||||
@@ -1293,6 +1294,12 @@ dependencies = [
|
|||||||
"paste",
|
"paste",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minisign-verify"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "871285dc19d8d0ebe0eef3d0e99a205f2a71363b122632cbbfa4a6c370a960ce"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ sha2 = "0.10"
|
|||||||
ureq = { version = "2.10", default-features = false, features = ["tls", "gzip", "json"] }
|
ureq = { version = "2.10", default-features = false, features = ["tls", "gzip", "json"] }
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
octotablet = "0.1.0"
|
octotablet = "0.1.0"
|
||||||
|
minisign-verify = "0.3.0"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 1 # Erhöht Rendering-Performance auch im Debug-Modus
|
opt-level = 1 # Erhöht Rendering-Performance auch im Debug-Modus
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ Eine latenzoptimierte, hardwarebeschleunigte Inking- und Notizbuch-Anwendung fü
|
|||||||
|
|
||||||
- **Natives .deb-Paket**: Optimiert für Zorin OS 18 (Ubuntu 24.04 LTS Basis) mit vollständiger XDG Desktop-Integration (`.desktop`, Hicolor-Icons SVG & PNG, AppStream Metainfo, Debian Copyright & Changelog).
|
- **Natives .deb-Paket**: Optimiert für Zorin OS 18 (Ubuntu 24.04 LTS Basis) mit vollständiger XDG Desktop-Integration (`.desktop`, Hicolor-Icons SVG & PNG, AppStream Metainfo, Debian Copyright & Changelog).
|
||||||
- **Portables .AppImage**: Eigenständiges x86_64 Bundle.
|
- **Portables .AppImage**: Eigenständiges x86_64 Bundle.
|
||||||
- **In-App Updater (`src/updater.rs`)**: Automatischer Prüflauf gegen Gitea Releases, SemVer-Filterung, SHA-256-Prüfung und atomarer In-Place Austausch.
|
- **In-App Updater (`src/updater.rs`)**: Automatischer Prüflauf gegen Gitea Releases, SemVer-Filterung, Minisign-Signaturprüfung, SHA-256-Prüfung und atomarer In-Place Austausch.
|
||||||
|
|
||||||
### 1. Release-Pakete bauen
|
### 1. Release-Pakete bauen
|
||||||
|
|
||||||
@@ -84,6 +84,23 @@ Ergebnis in `target/`:
|
|||||||
- `ink-notes_0.1.0_amd64.deb`
|
- `ink-notes_0.1.0_amd64.deb`
|
||||||
- `ink-notes_0.1.0_x86_64.AppImage`
|
- `ink-notes_0.1.0_x86_64.AppImage`
|
||||||
- `SHA256SUMS.txt`
|
- `SHA256SUMS.txt`
|
||||||
|
- `SHA256SUMS.txt.minisig` (nur mit gesetztem `MINISIGN_SECRET_KEY`, siehe unten)
|
||||||
|
|
||||||
|
### Release-Signatur einrichten (einmalig)
|
||||||
|
|
||||||
|
In-App-Updates werden nur mit gültiger Minisign-Signatur installiert. Einmalig ein Schlüsselpaar erzeugen (ohne Passwort, da es nur als CI-Secret verwendet wird):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
minisign -G -W -p ink-notes.pub -s ink-notes.key
|
||||||
|
# oder ohne minisign-Paket, mit dem kompatiblen rsign2 (cargo install rsign2):
|
||||||
|
rsign generate -W -p ink-notes.pub -s ink-notes.key
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Die zweite Zeile aus `ink-notes.pub` (beginnt mit `RW`) in `src/updater.rs` als `UPDATE_SIGNING_PUBLIC_KEY` eintragen und committen.
|
||||||
|
2. Den kompletten Inhalt von `ink-notes.key` in Gitea unter *Repository → Einstellungen → Actions → Secrets* als `MINISIGN_SECRET_KEY` anlegen.
|
||||||
|
3. `ink-notes.key` sicher offline aufbewahren und vom Rechner löschen. Geht der Schlüssel verloren, können bestehende Installationen keine Updates mehr prüfen.
|
||||||
|
|
||||||
|
Lokal signiert `build-release.sh` nur, wenn `MINISIGN_SECRET_KEY` gesetzt ist. In der CI ist die Signatur Pflicht (`INK_NOTES_REQUIRE_SIGNATURE=1`), und das Skript prüft, dass der geheime Schlüssel zum eingebauten öffentlichen Schlüssel passt.
|
||||||
|
|
||||||
### 2. Installation auf Zorin OS 18 / Ubuntu 24.04
|
### 2. Installation auf Zorin OS 18 / Ubuntu 24.04
|
||||||
|
|
||||||
@@ -123,15 +140,15 @@ export GITEA_TOKEN="<DEIN_GITEA_TOKEN>"
|
|||||||
|
|
||||||
Der In-App-Updater (`src/updater.rs`) folgt strengen Sicherheitsrichtlinien:
|
Der In-App-Updater (`src/updater.rs`) folgt strengen Sicherheitsrichtlinien:
|
||||||
|
|
||||||
1. **SHA-256-Integritätsprüfung:**
|
1. **Minisign-Signatur (Pflicht):**
|
||||||
- Vor jeder Ausführung oder Installation wird die offizielle Prüfsummendatei `SHA256SUMS.txt` aus dem Release bezogen.
|
- Jedes Release enthält `SHA256SUMS.txt.minisig`, eine Minisign-Signatur der Prüfsummendatei.
|
||||||
- Der SHA-256-Hash des heruntergeladenen Pakets wird berechnet (`sha2`).
|
- Der Updater lädt `SHA256SUMS.txt` und die Signatur **vor** dem Paket und prüft sie gegen den in die App eingebauten öffentlichen Schlüssel (`UPDATE_SIGNING_PUBLIC_KEY` in `src/updater.rs`, Crate `minisign-verify`).
|
||||||
|
- Fehlt die Signatur, ist sie ungültig oder ist kein Schlüssel eingebaut, wird das Update abgelehnt. Ein kompromittierter Release-Server kann so keine manipulierten Pakete ausliefern.
|
||||||
|
2. **SHA-256-Integritätsprüfung:**
|
||||||
|
- Der SHA-256-Hash des heruntergeladenen Pakets wird mit dem Eintrag in der signierten `SHA256SUMS.txt` verglichen (`sha2`).
|
||||||
- Weicht die Prüfsumme ab oder fehlt ein Eintrag, wird die Datei sofort gelöscht und der Installationsvorgang mit Fehlermeldung abgebrochen.
|
- Weicht die Prüfsumme ab oder fehlt ein Eintrag, wird die Datei sofort gelöscht und der Installationsvorgang mit Fehlermeldung abgebrochen.
|
||||||
- *Hinweis zur Authentizität:* Eine Prüfsumme vom selben Release-Server garantiert Schutz gegen Übertragungsfehler und Datei-Beschädigungen, jedoch keine kryptografische Nicht-Abstreitbarkeit gegenüber einem kompromittierten Server.
|
|
||||||
2. **Signatur-Ausblick:**
|
|
||||||
- Die Updater-Architektur enthält Schnittstellen für optionale `.sig`/`.minisig`-Signaturen zur zukünftigen End-to-End-Kryptoverifikation über öffentliche Schlüssel.
|
|
||||||
3. **Redirect-Schutz & HTTPS:**
|
3. **Redirect-Schutz & HTTPS:**
|
||||||
- Alle API- und Download-Endpunkte erzwingen standardmäßig HTTPS.
|
- Jede Anfrage erzwingt HTTPS – auch Download-Links aus der Release-API und jedes Weiterleitungsziel.
|
||||||
- Bei HTTP-Redirects werden sensible Autorisierungs-Header (`Authorization: token ...`) verworfen, sobald die Weiterleitung auf eine andere Origin oder ein externes CDN verweist.
|
- Bei HTTP-Redirects werden sensible Autorisierungs-Header (`Authorization: token ...`) verworfen, sobald die Weiterleitung auf eine andere Origin oder ein externes CDN verweist.
|
||||||
4. **Isolierter Download-Pfad & Path-Traversal-Schutz:**
|
4. **Isolierter Download-Pfad & Path-Traversal-Schutz:**
|
||||||
- Downloads werden in einem isolierten Benutzerverzeichnis abgelegt (`$XDG_CACHE_HOME/ink-notes/updates` bzw. `~/.cache/ink-notes/updates`, Berechtigung `0700`).
|
- Downloads werden in einem isolierten Benutzerverzeichnis abgelegt (`$XDG_CACHE_HOME/ink-notes/updates` bzw. `~/.cache/ink-notes/updates`, Berechtigung `0700`).
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Haupt-Release-Build-Skript für ink-notes (Zorin OS 18 / Linux)
|
# Haupt-Release-Build-Skript für ink-notes (Zorin OS 18 / Linux)
|
||||||
# Erstellt .deb, .AppImage und SHA256-Prüfsummen in target/
|
# Erstellt .deb, .AppImage, SHA256-Prüfsummen und deren Minisign-Signatur in target/
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -30,10 +30,36 @@ echo "=== Berechne SHA256-Prüfsummen in target/${CHECKSUM_FILE} ==="
|
|||||||
cd "${ROOT_DIR}/target"
|
cd "${ROOT_DIR}/target"
|
||||||
sha256sum "${DEB_NAME}" "${APPIMAGE_NAME}" > "${CHECKSUM_FILE}"
|
sha256sum "${DEB_NAME}" "${APPIMAGE_NAME}" > "${CHECKSUM_FILE}"
|
||||||
|
|
||||||
|
# 4. Prüfsummen mit Minisign signieren (Pflicht für In-App-Updates)
|
||||||
|
SIGNATURE_FILE="${CHECKSUM_FILE}.minisig"
|
||||||
|
rm -f "${SIGNATURE_FILE}"
|
||||||
|
EMBEDDED_PUBKEY=$(grep -m1 'pub const UPDATE_SIGNING_PUBLIC_KEY' "${ROOT_DIR}/src/updater.rs" | cut -d '"' -f2)
|
||||||
|
if [ -n "${MINISIGN_SECRET_KEY:-}" ]; then
|
||||||
|
if [ -z "${EMBEDDED_PUBKEY}" ]; then
|
||||||
|
echo "FEHLER: UPDATE_SIGNING_PUBLIC_KEY in src/updater.rs ist leer – diese Version könnte keine Updates prüfen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "=== Signiere ${CHECKSUM_FILE} mit Minisign ==="
|
||||||
|
KEY_FILE="$(mktemp)"
|
||||||
|
trap 'rm -f "${KEY_FILE}"' EXIT
|
||||||
|
printf '%s\n' "${MINISIGN_SECRET_KEY}" > "${KEY_FILE}"
|
||||||
|
minisign -S -s "${KEY_FILE}" -m "${CHECKSUM_FILE}" -t "ink-notes v${VERSION}"
|
||||||
|
rm -f "${KEY_FILE}"
|
||||||
|
# Stellt sicher, dass der geheime Schlüssel zum eingebauten öffentlichen Schlüssel passt
|
||||||
|
minisign -V -P "${EMBEDDED_PUBKEY}" -m "${CHECKSUM_FILE}"
|
||||||
|
elif [ "${INK_NOTES_REQUIRE_SIGNATURE:-0}" = "1" ]; then
|
||||||
|
echo "FEHLER: MINISIGN_SECRET_KEY ist nicht gesetzt, eine Signatur ist aber Pflicht."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "WARNUNG: MINISIGN_SECRET_KEY nicht gesetzt – ${CHECKSUM_FILE} bleibt unsigniert."
|
||||||
|
echo " Installierte Apps lehnen ein solches Release als Update ab."
|
||||||
|
fi
|
||||||
|
|
||||||
echo "======================================================================"
|
echo "======================================================================"
|
||||||
echo "Release-Artefakte für v${VERSION} erfolgreich erstellt:"
|
echo "Release-Artefakte für v${VERSION} erfolgreich erstellt:"
|
||||||
echo "======================================================================"
|
echo "======================================================================"
|
||||||
ls -lh "${DEB_NAME}" "${APPIMAGE_NAME}" "${CHECKSUM_FILE}"
|
ls -lh "${DEB_NAME}" "${APPIMAGE_NAME}" "${CHECKSUM_FILE}"
|
||||||
|
if [ -f "${SIGNATURE_FILE}" ]; then ls -lh "${SIGNATURE_FILE}"; fi
|
||||||
echo ""
|
echo ""
|
||||||
echo "Inhalt von target/${CHECKSUM_FILE}:"
|
echo "Inhalt von target/${CHECKSUM_FILE}:"
|
||||||
cat "${CHECKSUM_FILE}"
|
cat "${CHECKSUM_FILE}"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Veröffentlicht ein Release auf dem Gitea-Server (gitea.pansi.eu)
|
# Veröffentlicht ein Release auf dem Gitea-Server (gitea.pansi.eu)
|
||||||
# Lädt .deb, .AppImage und SHA256SUMS.txt idempotent als Release-Assets hoch.
|
# Lädt .deb, .AppImage, SHA256SUMS.txt und SHA256SUMS.txt.minisig idempotent als Release-Assets hoch.
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ RELEASE_TITLE="ink-notes ${TAG_NAME}"
|
|||||||
DEB_FILE="${ROOT_DIR}/target/ink-notes_${VERSION}_amd64.deb"
|
DEB_FILE="${ROOT_DIR}/target/ink-notes_${VERSION}_amd64.deb"
|
||||||
APPIMAGE_FILE="${ROOT_DIR}/target/ink-notes_${VERSION}_x86_64.AppImage"
|
APPIMAGE_FILE="${ROOT_DIR}/target/ink-notes_${VERSION}_x86_64.AppImage"
|
||||||
CHECKSUMS_FILE="${ROOT_DIR}/target/SHA256SUMS.txt"
|
CHECKSUMS_FILE="${ROOT_DIR}/target/SHA256SUMS.txt"
|
||||||
|
SIGNATURE_FILE="${CHECKSUMS_FILE}.minisig"
|
||||||
TEMPLATE_FILE="${ROOT_DIR}/packaging/release-notes.md.tmpl"
|
TEMPLATE_FILE="${ROOT_DIR}/packaging/release-notes.md.tmpl"
|
||||||
|
|
||||||
if [ ! -f "${TEMPLATE_FILE}" ]; then
|
if [ ! -f "${TEMPLATE_FILE}" ]; then
|
||||||
@@ -46,6 +47,12 @@ if [ ! -f "${DEB_FILE}" ] || [ ! -f "${APPIMAGE_FILE}" ] || [ ! -f "${CHECKSUMS_
|
|||||||
"${SCRIPT_DIR}/build-release.sh"
|
"${SCRIPT_DIR}/build-release.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Ohne Signatur lehnen installierte Apps das Release als Update ab
|
||||||
|
if [ ! -f "${SIGNATURE_FILE}" ]; then
|
||||||
|
echo "FEHLER: '${SIGNATURE_FILE}' fehlt. Release mit gesetztem MINISIGN_SECRET_KEY bauen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "======================================================================"
|
echo "======================================================================"
|
||||||
echo "Veröffentliche Release ${TAG_NAME} auf ${GITEA_SERVER}/${REPO_OWNER}/${REPO_NAME}"
|
echo "Veröffentliche Release ${TAG_NAME} auf ${GITEA_SERVER}/${REPO_OWNER}/${REPO_NAME}"
|
||||||
echo "======================================================================"
|
echo "======================================================================"
|
||||||
@@ -230,6 +237,7 @@ except Exception:
|
|||||||
upload_asset "${DEB_FILE}"
|
upload_asset "${DEB_FILE}"
|
||||||
upload_asset "${APPIMAGE_FILE}"
|
upload_asset "${APPIMAGE_FILE}"
|
||||||
upload_asset "${CHECKSUMS_FILE}"
|
upload_asset "${CHECKSUMS_FILE}"
|
||||||
|
upload_asset "${SIGNATURE_FILE}"
|
||||||
|
|
||||||
echo "======================================================================"
|
echo "======================================================================"
|
||||||
echo "Release ${TAG_NAME} erfolgreich veröffentlicht!"
|
echo "Release ${TAG_NAME} erfolgreich veröffentlicht!"
|
||||||
|
|||||||
+148
-59
@@ -7,7 +7,7 @@
|
|||||||
//! - Sichere Asset-Namensbereinigung (Path Traversal Schutz)
|
//! - Sichere Asset-Namensbereinigung (Path Traversal Schutz)
|
||||||
//! - Isolierter Benutzer-Cache-Pfad (`$XDG_CACHE_HOME/ink-notes/updates` mit 0700 Rechten)
|
//! - Isolierter Benutzer-Cache-Pfad (`$XDG_CACHE_HOME/ink-notes/updates` mit 0700 Rechten)
|
||||||
//! - Integritätsprüfung via SHA-256 (`SHA256SUMS.txt`) mit automatischem Löschen ungültiger Dateien
|
//! - Integritätsprüfung via SHA-256 (`SHA256SUMS.txt`) mit automatischem Löschen ungültiger Dateien
|
||||||
//! - Erweiterbare Schnittstelle für optionale Signaturprüfung (.sig / .minisig)
|
//! - Pflicht-Signaturprüfung der `SHA256SUMS.txt` via Minisign (eingebauter öffentlicher Schlüssel)
|
||||||
//! - Atomares AppImage-Self-Update via `.new`-Zwischendatei, Backup-Pflicht und atomarem `fs::rename`
|
//! - Atomares AppImage-Self-Update via `.new`-Zwischendatei, Backup-Pflicht und atomarem `fs::rename`
|
||||||
//! - Echtes Fortschritts-Streaming (~10 Updates/s)
|
//! - Echtes Fortschritts-Streaming (~10 Updates/s)
|
||||||
//! - Korrekte SemVer-Prüfung via `semver` Crate (Prerelease-Schutz standardmäßig aktiv)
|
//! - Korrekte SemVer-Prüfung via `semver` Crate (Prerelease-Schutz standardmäßig aktiv)
|
||||||
@@ -313,22 +313,77 @@ pub fn verify_checksum(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Status einer Signaturverifikation (.sig / .minisig).
|
/// Öffentlicher Minisign-Schlüssel (Base64), mit dem die `SHA256SUMS.txt` jedes Releases
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
/// signiert sein muss. Ist er leer, lehnt diese Version alle In-App-Updates ab.
|
||||||
pub enum SignatureVerificationStatus {
|
/// Erzeugen: `minisign -G -W -p ink-notes.pub -s ink-notes.key`, dann die zweite Zeile
|
||||||
NotPresent,
|
/// aus `ink-notes.pub` hier eintragen (siehe README, Abschnitt Release-Signatur).
|
||||||
Verified { signer: String },
|
pub const UPDATE_SIGNING_PUBLIC_KEY: &str = "";
|
||||||
Failed(String),
|
|
||||||
|
/// Name des Prüfsummen-Assets eines Releases.
|
||||||
|
pub const CHECKSUMS_ASSET_NAME: &str = "SHA256SUMS.txt";
|
||||||
|
/// Name der Minisign-Signatur zu [`CHECKSUMS_ASSET_NAME`].
|
||||||
|
pub const CHECKSUMS_SIGNATURE_ASSET_NAME: &str = "SHA256SUMS.txt.minisig";
|
||||||
|
|
||||||
|
/// Prüft die Minisign-Signatur der Prüfsummendatei. Nur aktuelle (prehashed) Signaturen
|
||||||
|
/// werden akzeptiert; ohne hinterlegten Schlüssel schlägt die Prüfung immer fehl.
|
||||||
|
pub fn verify_checksums_signature(
|
||||||
|
checksums: &[u8],
|
||||||
|
signature_text: &str,
|
||||||
|
public_key_b64: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let key = public_key_b64.trim();
|
||||||
|
if key.is_empty() {
|
||||||
|
return Err("In dieser Version ist kein Signaturschlüssel für Updates hinterlegt. Bitte das Update manuell installieren.".to_string());
|
||||||
|
}
|
||||||
|
let public_key = minisign_verify::PublicKey::from_base64(key)
|
||||||
|
.map_err(|e| format!("Ungültiger Update-Signaturschlüssel: {e}."))?;
|
||||||
|
let signature = minisign_verify::Signature::decode(signature_text)
|
||||||
|
.map_err(|e| format!("{CHECKSUMS_SIGNATURE_ASSET_NAME} ist ungültig: {e}."))?;
|
||||||
|
public_key
|
||||||
|
.verify(checksums, &signature, false)
|
||||||
|
.map_err(|e| format!("Signaturprüfung von {CHECKSUMS_ASSET_NAME} fehlgeschlagen: {e}."))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Erweiterbare Schnittstelle für kryptografische Signaturprüfung.
|
/// Lädt einen Text-Asset des Releases über den abgesicherten HTTP-Client.
|
||||||
pub fn verify_signature_if_present(
|
fn fetch_release_text(
|
||||||
_asset_file: &Path,
|
agent: &ureq::Agent,
|
||||||
_sig_bytes: Option<&[u8]>,
|
release: &GiteaRelease,
|
||||||
) -> Result<SignatureVerificationStatus, String> {
|
name: &str,
|
||||||
// Erweiterungspunkt: Sobald ein Signaturschlüssel / Minisign-Public-Key
|
config: &UpdaterConfig,
|
||||||
// im Projekt konfiguriert ist, kann hier die kryptografische Verifikation erfolgen.
|
) -> Result<String, String> {
|
||||||
Ok(SignatureVerificationStatus::NotPresent)
|
let asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name == name)
|
||||||
|
.ok_or_else(|| format!("Release enthält keine {name}."))?;
|
||||||
|
http_get_with_auth_guard(
|
||||||
|
agent,
|
||||||
|
&asset.browser_download_url,
|
||||||
|
config.auth_token.as_deref(),
|
||||||
|
5,
|
||||||
|
config.allow_insecure_http,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Konnte {name} nicht laden: {e}."))?
|
||||||
|
.into_string()
|
||||||
|
.map_err(|e| format!("Konnte {name} nicht dekodieren: {e}."))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lädt `SHA256SUMS.txt` samt Minisign-Signatur und gibt den Inhalt nur bei gültiger
|
||||||
|
/// Signatur zurück.
|
||||||
|
fn fetch_verified_checksums(
|
||||||
|
agent: &ureq::Agent,
|
||||||
|
release: &GiteaRelease,
|
||||||
|
config: &UpdaterConfig,
|
||||||
|
public_key_b64: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
// Ohne Schlüssel gar nicht erst Netzwerkanfragen stellen
|
||||||
|
if public_key_b64.trim().is_empty() {
|
||||||
|
return verify_checksums_signature(b"", "", public_key_b64).map(|()| String::new());
|
||||||
|
}
|
||||||
|
let checksums = fetch_release_text(agent, release, CHECKSUMS_ASSET_NAME, config)?;
|
||||||
|
let signature = fetch_release_text(agent, release, CHECKSUMS_SIGNATURE_ASSET_NAME, config)?;
|
||||||
|
verify_checksums_signature(checksums.as_bytes(), &signature, public_key_b64)?;
|
||||||
|
Ok(checksums)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event-Typen für den asynchronen Worker.
|
/// Event-Typen für den asynchronen Worker.
|
||||||
@@ -583,7 +638,31 @@ impl UpdaterController {
|
|||||||
let target_file = cache_dir.join(&safe_asset_name);
|
let target_file = cache_dir.join(&safe_asset_name);
|
||||||
let agent = create_http_agent();
|
let agent = create_http_agent();
|
||||||
|
|
||||||
// 1. Download des Release-Assets mit Fortschritts-Streaming
|
// 1. Signierte Prüfsummen laden und verifizieren, BEVOR das Paket geladen wird
|
||||||
|
let checksums_content = match active_release.as_ref() {
|
||||||
|
Some(release) => match fetch_verified_checksums(
|
||||||
|
&agent,
|
||||||
|
release,
|
||||||
|
&config,
|
||||||
|
UPDATE_SIGNING_PUBLIC_KEY,
|
||||||
|
) {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = tx.send(UpdaterEvent::DownloadError(format!(
|
||||||
|
"{e} Download abgelehnt."
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let _ = tx.send(UpdaterEvent::DownloadError(
|
||||||
|
"Kein Release ausgewählt. Download abgelehnt.".to_string(),
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Download des Release-Assets mit Fortschritts-Streaming
|
||||||
let resp = match http_get_with_auth_guard(
|
let resp = match http_get_with_auth_guard(
|
||||||
&agent,
|
&agent,
|
||||||
&asset.browser_download_url,
|
&asset.browser_download_url,
|
||||||
@@ -667,54 +746,13 @@ impl UpdaterController {
|
|||||||
|
|
||||||
drop(file);
|
drop(file);
|
||||||
|
|
||||||
// 2. Integritätsprüfung gegen SHA256SUMS.txt
|
// 3. Integritätsprüfung des Pakets gegen die signierte SHA256SUMS.txt
|
||||||
let checksum_asset = active_release
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|r| r.assets.iter().find(|a| a.name == "SHA256SUMS.txt"));
|
|
||||||
|
|
||||||
let checksums_content = match checksum_asset {
|
|
||||||
Some(ca) => {
|
|
||||||
match http_get_with_auth_guard(
|
|
||||||
&agent,
|
|
||||||
&ca.browser_download_url,
|
|
||||||
config.auth_token.as_deref(),
|
|
||||||
5,
|
|
||||||
config.allow_insecure_http,
|
|
||||||
) {
|
|
||||||
Ok(c_resp) => match c_resp.into_string() {
|
|
||||||
Ok(text) => text,
|
|
||||||
Err(e) => {
|
|
||||||
let _ = std::fs::remove_file(&target_file);
|
|
||||||
let _ = tx.send(UpdaterEvent::DownloadError(format!(
|
|
||||||
"Konnte SHA256SUMS.txt nicht dekodieren: {e}. Datei gelöscht."
|
|
||||||
)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
let _ = std::fs::remove_file(&target_file);
|
|
||||||
let _ = tx.send(UpdaterEvent::DownloadError(format!(
|
|
||||||
"Konnte SHA256SUMS.txt nicht laden: {e}. Datei aus Sicherheitsgründen gelöscht."
|
|
||||||
)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let _ = std::fs::remove_file(&target_file);
|
|
||||||
let _ = tx.send(UpdaterEvent::DownloadError(
|
|
||||||
"Release enthält keine SHA256SUMS.txt-Prüfsummen. Download abgelehnt und gelöscht.".to_string(),
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = verify_checksum(&target_file, &safe_asset_name, &checksums_content) {
|
if let Err(err) = verify_checksum(&target_file, &safe_asset_name, &checksums_content) {
|
||||||
let _ = tx.send(UpdaterEvent::DownloadError(err));
|
let _ = tx.send(UpdaterEvent::DownloadError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Wenn es ein AppImage ist, Executable-Bit auf 0755 setzen
|
// 4. Wenn es ein AppImage ist, Executable-Bit auf 0755 setzen
|
||||||
if safe_asset_name.ends_with(".AppImage") {
|
if safe_asset_name.ends_with(".AppImage") {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
@@ -1018,6 +1056,57 @@ dcdeb7ac87d9e6cb8022e5a71bc7f2c56d779b4fce4efdd5bae5be3b0214884a ink-notes_0.1.
|
|||||||
assert!(allowed_insecure.validate_url().is_ok());
|
assert!(allowed_insecure.validate_url().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Nur für Tests erzeugtes Minisign-Schlüsselpaar (der geheime Schlüssel wurde verworfen).
|
||||||
|
const TEST_PUBLIC_KEY: &str = "RWQr5S8CVgsjS4UA3uY0xqTDua4uRSSzKWpFfJKLCypjgP3NGt9Tmdbh";
|
||||||
|
const TEST_CHECKSUMS: &str = "abc123 ink-notes_9.9.9_amd64.deb\n";
|
||||||
|
const TEST_SIGNATURE: &str = "untrusted comment: signature from rsign secret key
|
||||||
|
RUQr5S8CVgsjS5BYMUx1ICe1E356aP7wesQXw+oKF6Rd8WsvdF1dmCb1ffonXgJ2GhWE5iAWPXQaT8Y/Y4zLIpyS1PA8JCe8eAM=
|
||||||
|
trusted comment: ink-notes test
|
||||||
|
iw0NOedyAQg3emO36fHjyfn+AbnI7Idyl+NwFTDJX1YceLM1jSfjIfeIc0D49IQXyGTpM/QnkZJkRO/eI8ZKBQ==
|
||||||
|
";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_checksums_signature_verification() {
|
||||||
|
// Gültige Signatur
|
||||||
|
assert!(verify_checksums_signature(
|
||||||
|
TEST_CHECKSUMS.as_bytes(),
|
||||||
|
TEST_SIGNATURE,
|
||||||
|
TEST_PUBLIC_KEY
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
|
||||||
|
// Manipulierte Prüfsummen (z. B. ausgetauschter Hash)
|
||||||
|
let tampered = TEST_CHECKSUMS.replace("abc123", "abc124");
|
||||||
|
assert!(
|
||||||
|
verify_checksums_signature(tampered.as_bytes(), TEST_SIGNATURE, TEST_PUBLIC_KEY)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fremder Schlüssel
|
||||||
|
let other_key = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3";
|
||||||
|
assert!(
|
||||||
|
verify_checksums_signature(TEST_CHECKSUMS.as_bytes(), TEST_SIGNATURE, other_key)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kein Schlüssel hinterlegt -> strikt ablehnen
|
||||||
|
let err =
|
||||||
|
verify_checksums_signature(TEST_CHECKSUMS.as_bytes(), TEST_SIGNATURE, "").unwrap_err();
|
||||||
|
assert!(err.contains("kein Signaturschlüssel"), "{err}");
|
||||||
|
|
||||||
|
// Fehlende / kaputte Signatur
|
||||||
|
assert!(
|
||||||
|
verify_checksums_signature(TEST_CHECKSUMS.as_bytes(), "", TEST_PUBLIC_KEY).is_err()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Altes (nicht prehashed) Signaturformat wird nicht akzeptiert
|
||||||
|
let legacy_sig = "untrusted comment: signature from minisign secret key
|
||||||
|
RWQf6LRCGA9i59SLOFxz6NxvASXDJeRtuZykwQepbDEGt87ig1BNpWaVWuNrm73YiIiJbq71Wi+dP9eKL8OC351vwIasSSbXxwA=
|
||||||
|
trusted comment: timestamp:1555779966\tfile:test
|
||||||
|
QtKMXWyYcwdpZAlPF7tE2ENJkRd1ujvKjlj1m9RtHTBnZPa5WKU5uWRs5GoP5M/VqE81QFuMKI5k/SfNQUaOAA==";
|
||||||
|
assert!(verify_checksums_signature(b"test", legacy_sig, other_key).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_https_enforced_for_every_request() {
|
fn test_https_enforced_for_every_request() {
|
||||||
let https = url::Url::parse("https://gitea.pansi.eu/a.deb").unwrap();
|
let https = url::Url::parse("https://gitea.pansi.eu/a.deb").unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user