feat(desktop): signature updater Tauri + wizard persistant + protocole E2E (#77)
CI / lint (push) Successful in 1m11s
CI / security (push) Successful in 43s
CI / test (push) Successful in 2m22s
CI / build (push) Successful in 1m41s
CI / e2e (push) Successful in 10m50s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Successful in 1m11s
CI / security (push) Successful in 43s
CI / test (push) Successful in 2m22s
CI / build (push) Successful in 1m41s
CI / e2e (push) Successful in 10m50s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
- Signature des mises a jour Tauri : paire de cles minisign generee, cle publique dans tauri.conf.json, createUpdaterArtifacts actif, secrets CI exposes (repli build non signe si secret absent). - Wizard 1er lancement : etat persistant wizard_done cote Rust (get_wizard_state/complete_wizard) pour ne plus reafficher la banniere, retro-compatible. - Protocole des 6 tests E2E manuels : docs/DESKTOP_E2E_CHECKLIST.md. - CI : desktop.test.mjs ajoute au job lint. - Docs : ROADMAP, fiche desktop-tauri, CHANGELOG, README desktop, guide releases. - fix(build): retirer les libs WeasyPrint inutiles du stage builder Docker.
This commit is contained in:
@@ -12,6 +12,7 @@ Application desktop native pour [ObsiGate](https://git.dracodev.net/Projets/Obsi
|
||||
- [Configuration des vaults](#configuration-des-vaults)
|
||||
- [Architecture](#architecture)
|
||||
- [Fonctionnalités natives](#fonctionnalites-natives)
|
||||
- [Signature de code Windows](#signature-de-code-windows)
|
||||
- [Dépannage](#depannage)
|
||||
|
||||
---
|
||||
@@ -49,6 +50,11 @@ sudo apt install libwebkit2gtk-4.1-0 libgtk-3-0 libayatana-appindicator3-1
|
||||
:: Mêmes options, inclut le raccourci
|
||||
```
|
||||
|
||||
> **Binaires non signés** : les releases ne sont pas signées avec un certificat
|
||||
> Windows. Au premier lancement, SmartScreen affiche « Windows a protégé votre
|
||||
> PC » → cliquer **Informations complémentaires → Exécuter quand même**.
|
||||
> Voir [Signature de code](#signature-de-code-windows) pour les alternatives.
|
||||
|
||||
### macOS
|
||||
|
||||
> Non supporté pour le moment (priorité Linux/Windows).
|
||||
@@ -260,6 +266,58 @@ Arrêt (tray → Quitter ou Ctrl+C) :
|
||||
|
||||
---
|
||||
|
||||
## Signature de code Windows
|
||||
|
||||
La signature de code est **optionnelle** : sans elle, l'application fonctionne,
|
||||
mais SmartScreen affiche un avertissement au premier lancement. Les binaires
|
||||
ObsiGate sont actuellement distribués **non signés**.
|
||||
|
||||
### Signer localement
|
||||
|
||||
Le script `scripts/sign-windows.ps1` signe le binaire et les installeurs après un
|
||||
`cargo tauri build`. Il lit les identifiants depuis l'environnement (jamais
|
||||
commités) et est un **no-op explicite** si aucun certificat n'est fourni :
|
||||
|
||||
```powershell
|
||||
$env:OBSIGATE_SIGN_CERT_PFX = "C:\certs\obsigate.pfx"
|
||||
$env:OBSIGATE_SIGN_CERT_PASSWORD = "..."
|
||||
$env:OBSIGATE_SIGN_TIMESTAMP_URL = "http://timestamp.digicert.com"
|
||||
.\scripts\sign-windows.ps1
|
||||
```
|
||||
|
||||
### Alternatives au certificat
|
||||
|
||||
| Option | Coût indicatif | Effet SmartScreen |
|
||||
|---|---|---|
|
||||
| **Livrer non signé** (statu quo) | 0 € | Avertissement → « Exécuter quand même » |
|
||||
| **SignPath.io** (projet open source) | Gratuit si éligible OSS | Réputation gérée par le service |
|
||||
| **Certum Open Source Code Signing** | ~70-100 €/an | Réputation progressive |
|
||||
| **Certificat OV** | ~150-400 €/an | Avertit tant que la réputation n'est pas établie |
|
||||
| **Certificat EV** | ~300-700 €/an + token USB/HSM | Réputation **immédiate** |
|
||||
| **Azure Trusted Signing** | ~10 $/mois | Bonne réputation, signature cloud |
|
||||
| **Certificat auto-signé** | 0 € | Inutile en distribution publique |
|
||||
|
||||
### Signature de l'auto-update (gratuite)
|
||||
|
||||
Indépendante de la signature Windows, la signature des mises à jour Tauri repose
|
||||
sur une paire de clés que vous générez vous-même :
|
||||
|
||||
```bash
|
||||
cargo tauri signer generate -w ~/.tauri/obsigate.key
|
||||
```
|
||||
|
||||
- La **clé publique** est déjà renseignée dans `plugins.updater.pubkey`
|
||||
(`tauri.conf.json`).
|
||||
- La **clé privée** doit être exposée au build via `TAURI_SIGNING_PRIVATE_KEY`
|
||||
(jamais commitée) ; dans le CI, via les secrets Gitea
|
||||
`TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`.
|
||||
- Le CLI produit des `.sig` par artefact (`*.msi.sig`, `*.AppImage.sig`, …).
|
||||
|
||||
> Le manifeste `latest.json` consommé par l'updater n'est pas généré par le CLI :
|
||||
> voir [DEVELOPMENT_AND_RELEASES §2bis](../docs/DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri).
|
||||
|
||||
---
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Le backend ne démarre pas
|
||||
|
||||
@@ -78,6 +78,11 @@ struct AppConfig {
|
||||
window_y: Option<f64>,
|
||||
window_width: Option<f64>,
|
||||
window_height: Option<f64>,
|
||||
/// True once the user has picked a vault or dismissed the first-run wizard.
|
||||
/// `#[serde(default)]` keeps existing config files (written before this
|
||||
/// field existed) parseable instead of falling back to a full reset.
|
||||
#[serde(default)]
|
||||
wizard_done: bool,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
@@ -90,6 +95,7 @@ impl Default for AppConfig {
|
||||
window_y: None,
|
||||
window_width: Some(1200.0),
|
||||
window_height: Some(800.0),
|
||||
wizard_done: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,6 +292,7 @@ fn get_config() -> AppConfig {
|
||||
fn save_vault_path(path: String) -> Result<(), String> {
|
||||
let mut config = load_config();
|
||||
config.vault_path = Some(path);
|
||||
config.wizard_done = true;
|
||||
save_config(&config);
|
||||
info!("Vault path saved: {}", config.vault_path.as_deref().unwrap_or("none"));
|
||||
Ok(())
|
||||
@@ -305,6 +312,7 @@ async fn pick_vault_folder(app: tauri::AppHandle) -> Result<String, String> {
|
||||
let p_str = p.to_string();
|
||||
let mut config = load_config();
|
||||
config.vault_path = Some(p_str.clone());
|
||||
config.wizard_done = true;
|
||||
save_config(&config);
|
||||
Ok(p_str)
|
||||
}
|
||||
@@ -312,6 +320,20 @@ async fn pick_vault_folder(app: tauri::AppHandle) -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_wizard_state() -> bool {
|
||||
load_config().wizard_done
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn complete_wizard() -> Result<(), String> {
|
||||
let mut config = load_config();
|
||||
config.wizard_done = true;
|
||||
save_config(&config);
|
||||
info!("First-run wizard marked as completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_system_theme(app: tauri::AppHandle) -> String {
|
||||
use tauri::Theme;
|
||||
@@ -564,6 +586,8 @@ fn main() {
|
||||
get_config,
|
||||
save_vault_path,
|
||||
get_vault_path,
|
||||
get_wizard_state,
|
||||
complete_wizard,
|
||||
pick_vault_folder,
|
||||
get_system_theme,
|
||||
restart_backend,
|
||||
@@ -904,6 +928,7 @@ mod tests {
|
||||
assert!(c.dirs.is_empty());
|
||||
assert_eq!(c.window_width, Some(1200.0));
|
||||
assert_eq!(c.window_height, Some(800.0));
|
||||
assert!(!c.wizard_done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -919,6 +944,7 @@ mod tests {
|
||||
window_y: Some(200.0),
|
||||
window_width: Some(1400.0),
|
||||
window_height: Some(900.0),
|
||||
wizard_done: true,
|
||||
};
|
||||
let loaded = roundtrip_config(&c);
|
||||
assert_eq!(loaded.vault_path, Some("/test/vault".into()));
|
||||
@@ -927,6 +953,33 @@ mod tests {
|
||||
assert_eq!(loaded.vaults[1].path, "/v/work");
|
||||
assert_eq!(loaded.dirs.len(), 1);
|
||||
assert_eq!(loaded.window_x, Some(100.0));
|
||||
assert!(loaded.wizard_done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_without_wizard_done_parses() {
|
||||
// Backward compatibility: config files written before `wizard_done`
|
||||
// existed must still load (instead of resetting to defaults).
|
||||
let json = r#"{
|
||||
"vault_path": "/old/vault",
|
||||
"vaults": [{"name": "Old", "path": "/old"}],
|
||||
"dirs": [],
|
||||
"window_x": null,
|
||||
"window_y": null,
|
||||
"window_width": 1000.0,
|
||||
"window_height": 700.0
|
||||
}"#;
|
||||
let c: AppConfig = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(c.vaults.len(), 1);
|
||||
assert_eq!(c.vaults[0].name, "Old");
|
||||
assert!(!c.wizard_done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wizard_done_roundtrip() {
|
||||
let mut c = AppConfig::default();
|
||||
c.wizard_done = true;
|
||||
assert!(roundtrip_config(&c).wizard_done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"createUpdaterArtifacts": true,
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
@@ -83,7 +84,7 @@
|
||||
"endpoints": [
|
||||
"https://git.dracodev.net/api/v1/repos/Projets/ObsiGate/releases/latest"
|
||||
],
|
||||
"pubkey": "OBSIGATE_UPDATE_PUBKEY_PLACEHOLDER",
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDcwQjU2MDM4QUVEREY3NApSV1IwMysyS0ExWUxCK1RQMUgrYnplQWlpYWU2SHVYajVIUHhzNEFKLzZ2Z2puZW9pSUQ2RE8rUAo=",
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user