Compare commits

...
3 Commits
8 changed files with 219 additions and 80 deletions

No files matched your search

+3 -1
View File
@@ -640,7 +640,9 @@ bouton **Aide** des joueurs et la page **Demandes des joueurs**, puis le **mode
depuis la page **Signature** (propriétaires), qui montre aussi l'état de chaque contenu. Côté joueur,
`"keyringUrl"` et `"contentSigning"` (`"warn"` puis `"require"`) dans `launcher.json` (intégré à l'exe) ;
en « require », un manifest non signé, mal signé ou plus ancien que le dernier vu est refusé (le jeu installé
reste jouable), et un catalogue refusé est ignoré. Après le retrait d'une clé compromise, relevez
reste jouable), et un catalogue refusé est ignoré. Le trousseau est retéléchargé sans redémarrer le launcher quand
un contenu est signé par une clé absente ou non autorisée pour ce contenu (au plus une fois par minute), et au plus
toutes les 30 minutes (une clé retirée n'est plus acceptée par un launcher laissé ouvert). Après le retrait d'une clé compromise, relevez
`"keyringMinSequence"` au numéro du nouveau trousseau. Mise en place : `server/deploy/LISEZMOI.md`, étape 3 quinquies.
* **Page « Journal d'audit »** (permission `audit.read` : propriétaires et administrateurs) : connexions,
invitations, approbations, publications, modifications et refus, les plus récents d'abord (par pages
+9 -3
View File
@@ -911,7 +911,9 @@ plus avant la recherche des mises à jour des jeux** (pas d'attente pour `--unin
pendant une maintenance) et aussitôt après une publication depuis le menu de ce PC (`IAdminContext.NotifyMaintenanceChanged`).
8 Ko au plus, 8 s au plus ; règles contrôlées même si signé ; signature vérifiée par
`ContentTrustService.VerifyMaintenanceAsync` (autorisation `maintenance`, date de signature non antérieure) ; révision
inférieure à l'état connu refusée ; même fichier que la fois précédente : rien à revérifier. **Rien n'est gardé sur le
inférieure à l'état connu refusée ; même fichier que la fois précédente (déjà accepté) : rien à revérifier ; fichier
refusé : revérifié à chaque relecture (un trousseau re-signé avec l'autorisation `maintenance` pendant que le launcher
tourne est retéléchargé et pris en compte, 3.3.1). **Rien n'est gardé sur le
disque** : injoignable au démarrage = pas de maintenance (le jeu installé reste jouable hors ligne, un joueur sans
connexion n'est jamais bloqué) ; pendant que le launcher tourne, une coupure ou un fichier refusé garde le dernier état
vérifié, un 404 termine la maintenance.
@@ -1016,8 +1018,12 @@ Trousseau téléchargé au démarrage (en parallèle du catalogue), vérifié (s
s'il est plus ancien que le dernier vu, différent sous le même numéro, ou plus ancien que `keyringMinSequence`
(launcher.json intégré, à relever après le retrait d'une clé compromise) ; hors ligne : dernier trousseau vu
(revérifié à la lecture du cache). Aucune erreur de téléchargement (délai, réponse invalide, bogue) ne remonte à
l'appelant : le cache est utilisé. Contenu signé par une clé absente du trousseau : nouveau téléchargement, au plus
une fois toutes les 5 minutes. Manifest (`ManifestClient`) et catalogue (`CatalogService`, avant tout contrôle des valeurs)
l'appelant : le cache est utilisé. Nouveau téléchargement du trousseau (mêmes contrôles) : après un échec réseau
(1 min), quand un contenu est signé par une clé absente du trousseau **ou présente sans l'autorisation de ce contenu**
(clé ajoutée ou autorisation donnée depuis : au plus une fois par minute, `KeyChangeRetryDelay`), et quand le trousseau
en mémoire a plus de 30 minutes (`KeyringMaxAge`, à la vérification suivante : un launcher laissé ouvert cesse de faire
confiance à une clé retirée). Avant la 3.3.1, une autorisation ajoutée à une clé déjà connue n'était vue qu'au
redémarrage (mode maintenance refusé à chaque relecture après la re-signature du trousseau). Manifest (`ManifestClient`) et catalogue (`CatalogService`, avant tout contrôle des valeurs)
vérifiés : clé du trousseau autorisée, signature valide, **date de signature ≥ la dernière acceptée** pour ce
contenu (un serveur compromis ne peut pas resservir une ancienne version légitime mais vulnérable). « warn » :
journal seulement ; « require » : manifest refusé (`ContentSecurityException`, message « Mise à jour bloquée par
+8 -3
View File
@@ -286,8 +286,11 @@ Limites :
textes) ;
* un launcher accepte tout trousseau signé par la clé racine dont le numéro est au moins `keyringMinSequence`
et au moins celui du dernier trousseau qu'il a vu ;
* un appareil ajouté au trousseau pendant qu'un launcher tourne est connu de ce launcher au plus tard 5 minutes
après (nouveau téléchargement du trousseau) ou à son prochain démarrage.
* un appareil ajouté au trousseau, ou une autorisation ajoutée à un appareil, pendant qu'un launcher tourne est
connu de ce launcher dès le contenu suivant signé par cet appareil (nouveau téléchargement du trousseau, au plus une
fois par minute) ;
* un launcher laissé ouvert retélécharge le trousseau toutes les 30 minutes au plus (à la vérification suivante) : un
appareil retiré, ou une autorisation enlevée, n'y est plus accepté sans attendre son redémarrage.
La clé racine (`launcher-signing.key`) ne doit jamais aller sur le VPS ni dans Git, ni servir de clé
d'administrateur (le menu d'administration la refuse, quel que soit le nom du fichier) : c'est elle qui protège
@@ -481,7 +484,9 @@ Prérequis : étapes 3 bis (catalogue) et 3 quinquies (trousseau).
concernés y apparaît, signalée « AUTORISATIONS ÉLARGIES » par `tools\sign-keyring.bat` : c'est attendu.
5. **Activer** depuis la page « Maintenance » (message facultatif, confirmation), **désactiver** de même. Les launchers
relisent le fichier au démarrage (avant toute mise à jour automatique), puis toutes les 2 minutes (toutes les minutes
pendant une maintenance) ; celui de l'administrateur qui publie bascule aussitôt.
pendant une maintenance) ; celui de l'administrateur qui publie bascule aussitôt. Un launcher resté ouvert pendant
la re-signature du trousseau (point 4) prend la nouvelle autorisation en compte à la relecture suivante
(launcher 3.3.1 et suivants ; en 3.3.0, il fallait le redémarrer).
Comportement des launchers :
- **Hors ligne** ou fichier injoignable au démarrage : pas de maintenance (le jeu installé reste jouable hors ligne).
@@ -41,8 +41,23 @@ public sealed class ContentSecurityException : Exception
public sealed class ContentTrustService
{
private static readonly TimeSpan FetchTimeout = TimeSpan.FromSeconds(20);
private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1);
private static readonly TimeSpan UnknownKeyRetryDelay = TimeSpan.FromMinutes(5);
/// <summary>Nouvel essai après un échec réseau (trousseau jamais obtenu pendant ce démarrage).</summary>
public static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1);
/// <summary>
/// Nouveau téléchargement, au plus à ce rythme, quand un contenu est signé par une clé absente du trousseau ou non
/// autorisée pour ce contenu : clé ajoutée ou autorisation donnée (ex. « maintenance ») pendant que le launcher tourne.
/// Plus court que la relecture du mode maintenance, pour qu'une activation juste après la republication du trousseau
/// soit vue à la relecture suivante.
/// </summary>
public static readonly TimeSpan KeyChangeRetryDelay = TimeSpan.FromMinutes(1);
/// <summary>
/// Âge maximal du trousseau en mémoire : au-delà, il est retéléchargé à la vérification suivante (une clé retirée ou
/// une autorisation enlevée est prise en compte sans redémarrer le launcher).
/// </summary>
public static readonly TimeSpan KeyringMaxAge = TimeSpan.FromMinutes(30);
private static readonly TimeSpan MaxFutureSignedAt = TimeSpan.FromDays(1);
private readonly Func<Uri, long, CancellationToken, Task<byte[]>> _download;
@@ -82,12 +97,15 @@ public sealed class ContentTrustService
/// <summary>
/// Trousseau à utiliser : celui du serveur s'il est valide et pas plus ancien que le dernier vu, sinon le dernier
/// vu (cache), sinon null. Téléchargé une fois par démarrage (nouvel essai après un échec réseau, ou après quelques
/// minutes quand un contenu est signé par une clé encore inconnue : clé ajoutée pendant que le launcher tourne).
/// vu (cache), sinon null. Téléchargé au démarrage, puis de nouveau :
/// • après un échec réseau (<see cref="RetryDelay"/>) ;
/// • quand un contenu est signé par une clé absente ou non autorisée pour ce contenu (<see cref="KeyChangeRetryDelay"/>) ;
/// • quand il a plus de <see cref="KeyringMaxAge"/> (launcher ouvert longtemps).
/// Chaque nouveau trousseau passe par les mêmes contrôles (signature racine, numéro jamais en baisse).
/// </summary>
public Task<KeyringDocument?> GetKeyringAsync(CancellationToken ct = default) => GetKeyringAsync(false, ct);
private async Task<KeyringDocument?> GetKeyringAsync(bool unknownKey, CancellationToken ct)
private async Task<KeyringDocument?> GetKeyringAsync(bool keyChanged, CancellationToken ct)
{
Task<KeyringDocument?> task;
lock (_lock)
@@ -95,7 +113,8 @@ public sealed class ContentTrustService
var elapsed = _time.GetUtcNow() - _lastAttempt;
if (_keyringTask is null
|| (_keyringTask.IsCompleted && !_keyringFetched && elapsed > RetryDelay)
|| (_keyringTask.IsCompleted && unknownKey && elapsed > UnknownKeyRetryDelay))
|| (_keyringTask.IsCompleted && keyChanged && elapsed > KeyChangeRetryDelay)
|| (_keyringTask.IsCompleted && elapsed > KeyringMaxAge))
{
_lastAttempt = _time.GetUtcNow();
_keyringTask = FetchKeyringAsync();
@@ -151,7 +170,7 @@ public sealed class ContentTrustService
}
catch (Exception ex) when (ex is InvalidDataException or JsonException or NotSupportedException or FormatException)
{
lock (_lock) _keyringFetched = true; // contenu refusé : pas de nouvel essai pendant ce démarrage
lock (_lock) _keyringFetched = true; // contenu refusé : nouvel essai seulement aux conditions de GetKeyringAsync
Log.Error($"Trousseau du serveur refusé : {ex.Message}" + (cached is null ? "" : $" (le n° {cached.Sequence} déjà vu reste utilisé)."));
return cached;
}
@@ -274,8 +293,9 @@ public sealed class ContentTrustService
private async Task VerifyAsync(string kind, string id, string digest, ContentSignature? signature, string what, CancellationToken ct)
{
var keyring = await GetKeyringAsync(false, ct).ConfigureAwait(false);
// Clé inconnue (ajoutée depuis le démarrage) : nouveau téléchargement du trousseau, limité dans le temps.
if (keyring is not null && signature?.KeyId is { } keyId && !keyring.Keys.Any(k => string.Equals(k.KeyId, keyId, StringComparison.Ordinal)))
// Clé absente du trousseau, ou présente sans l'autorisation de ce contenu (clé ajoutée, ou autorisation donnée,
// depuis le dernier téléchargement) : nouveau téléchargement du trousseau, limité dans le temps.
if (keyring is not null && KeyMayBeNewer(keyring, kind, signature))
keyring = await GetKeyringAsync(true, ct).ConfigureAwait(false);
string problem;
@@ -322,6 +342,14 @@ public sealed class ContentTrustService
if (!Report(what, problem)) throw new ContentSecurityException($"{what} : {problem}.");
}
/// <summary>La signature vient d'une clé que ce trousseau ne connaît pas, ou n'autorise pas pour ce contenu.</summary>
private static bool KeyMayBeNewer(KeyringDocument keyring, string kind, ContentSignature? signature)
{
if (signature?.KeyId is not { } keyId) return false;
var key = keyring.Keys.FirstOrDefault(k => string.Equals(k.KeyId, keyId, StringComparison.Ordinal));
return key?.Scopes is not { } scopes || !scopes.Contains(ContentKinds.ScopeOf(kind), StringComparer.Ordinal);
}
// ------------------------------------------------------------------ État
private sealed class TrustState
+1 -1
View File
@@ -17,7 +17,7 @@
(1.0.0 → 1.0.1 → 1.1.0…). Les launchers des joueurs ne se mettent à jour que vers
une version STRICTEMENT supérieure à la leur.
-->
<Version>3.3.0</Version>
<Version>3.3.1</Version>
<Authors>Lanisys</Authors>
<Company>LanisysGames</Company>
<Copyright>© LanisysGames</Copyright>
@@ -271,6 +271,74 @@ public class ContentTrustTests
Assert.Equal(2, server.Requests);
}
[Fact]
public async Task A_scope_granted_while_the_launcher_runs_is_found_by_downloading_the_keyring_again()
{
using var folder = new TempFolder();
using var root = SigningKit.NewKey();
using var nicolas = SigningKit.NewKey();
var server = new FakeServer();
server.Serve(SigningKit.Keyring(root, 1, SigningKit.Entry(nicolas, "Nicolas", SigningScopes.Releases)));
var clock = new ManualClock();
var trust = Trust(server, folder.Path, root, ContentSigningMode.Require, time: clock);
var catalog = new CatalogDocument { Revision = 1, Games = new Dictionary<string, CatalogGame> { ["EZR"] = new() { Tagline = "Texte" } } };
catalog = catalog with
{
Signature = SigningKit.Sign(nicolas, ContentKinds.Catalog, ContentKinds.CatalogId, ContentSigning.CatalogDigest(catalog))
};
Assert.False(await trust.VerifyCatalogAsync(catalog)); // clé connue, sans l'autorisation « catalog »
Assert.Equal(1, server.Requests);
// Trousseau republié avec l'autorisation (ex. « maintenance » après la mise à jour 3.3) : vu sans redémarrer,
// au plus une fois par KeyChangeRetryDelay.
server.Serve(SigningKit.Keyring(root, 2, SigningKit.Entry(nicolas, "Nicolas", SigningScopes.Releases, SigningScopes.Catalog)));
Assert.False(await trust.VerifyCatalogAsync(catalog));
Assert.Equal(1, server.Requests);
clock.Now += ContentTrustService.KeyChangeRetryDelay + TimeSpan.FromSeconds(1);
Assert.True(await trust.VerifyCatalogAsync(catalog));
Assert.Equal(2, server.Requests);
Assert.Equal(2, trust.CachedKeyring!.Sequence);
}
[Fact]
public async Task A_launcher_left_open_downloads_the_keyring_again_and_stops_trusting_a_removed_key()
{
using var folder = new TempFolder();
using var root = SigningKit.NewKey();
using var nicolas = SigningKit.NewKey();
using var compromised = SigningKit.NewKey();
var server = new FakeServer();
server.Serve(SigningKit.Keyring(root, 1, SigningKit.Entry(nicolas, "Nicolas"), SigningKit.Entry(compromised, "PC volé")));
var clock = new ManualClock();
var trust = Trust(server, folder.Path, root, ContentSigningMode.Require, time: clock);
await trust.VerifyManifestAsync(Manifest("1.2", compromised), "EZR");
// Clé retirée du trousseau (n° 2) : encore acceptée tant que le trousseau en mémoire est récent…
server.Serve(SigningKit.Keyring(root, 2, SigningKit.Entry(nicolas, "Nicolas")));
clock.Now += TimeSpan.FromMinutes(10);
await trust.VerifyManifestAsync(Manifest("1.3", compromised), "EZR");
Assert.Equal(1, server.Requests);
// … puis refusée dès qu'il a dépassé son âge maximal (sans redémarrer le launcher).
clock.Now += ContentTrustService.KeyringMaxAge;
await Assert.ThrowsAsync<ContentSecurityException>(() => trust.VerifyManifestAsync(Manifest("1.4", compromised), "EZR"));
Assert.Equal(2, server.Requests);
await trust.VerifyManifestAsync(Manifest("1.4", nicolas), "EZR");
// Serveur injoignable au téléchargement suivant : dernier trousseau vu (n° 2) conservé, pas d'erreur.
server.Keyring = null;
clock.Now += ContentTrustService.KeyringMaxAge + TimeSpan.FromMinutes(1);
await trust.VerifyManifestAsync(Manifest("1.5", nicolas), "EZR");
Assert.Equal(3, server.Requests);
await Assert.ThrowsAsync<ContentSecurityException>(() => trust.VerifyManifestAsync(Manifest("1.6", compromised), "EZR"));
// Serveur compromis qui ressert l'ancien trousseau n° 1 : refusé (retour en arrière), la clé reste exclue.
server.Serve(SigningKit.Keyring(root, 1, SigningKit.Entry(nicolas, "Nicolas"), SigningKit.Entry(compromised, "PC volé")));
clock.Now += ContentTrustService.KeyringMaxAge + TimeSpan.FromMinutes(1);
await Assert.ThrowsAsync<ContentSecurityException>(() => trust.VerifyManifestAsync(Manifest("1.7", compromised), "EZR"));
Assert.Equal(2, trust.CachedKeyring!.Sequence);
}
[Fact]
public async Task A_signature_dated_far_in_the_future_does_not_block_later_releases()
{
@@ -166,6 +166,50 @@ public class MaintenanceServiceTests
Assert.Empty(changes);
}
/// <summary>Horloge avancée à la main (trousseau).</summary>
private sealed class ManualClock : TimeProvider
{
public DateTimeOffset Now { get; set; } = DateTimeOffset.UtcNow;
public override DateTimeOffset GetUtcNow() => Now;
}
[Fact]
public async Task A_maintenance_published_after_the_keyring_gained_the_scope_is_applied_without_restarting()
{
// Cas signalé en 3.3.0 : launcher ouvert, trousseau republié avec l'autorisation « maintenance » (étape 3 decies),
// puis maintenance activée. Le launcher gardait l'ancien trousseau (clé connue, sans l'autorisation) jusqu'au
// redémarrage : chaque relecture refusait la maintenance.
using var folder = new TempFolder();
using var root = SigningKit.NewKey();
using var admin = SigningKit.NewKey();
var server = new FakeServer
{
Keyring = Encoding.UTF8.GetBytes(Keyring.Serialize(SigningKit.Keyring(root, 1,
SigningKit.Entry(admin, "Nicolas", SigningScopes.Releases, SigningScopes.Catalog))))
};
var clock = new ManualClock();
var trust = new ContentTrustService(server.Download, folder.Path, KeyringUrl, new[] { SigningKit.Root(root) },
ContentSigningMode.Require, time: clock);
var service = new MaintenanceService(server.Download, MaintenanceUrl, trust, clock);
var changes = new List<MaintenanceDocument?>();
service.Changed += changes.Add;
// Démarrage : rien de publié, trousseau n° 1 en mémoire.
Assert.NotNull(await trust.GetKeyringAsync());
Assert.False(await service.RefreshAsync());
// Trousseau n° 2 (autorisation « maintenance » ajoutée), puis maintenance activée quelques minutes plus tard.
server.Keyring = Encoding.UTF8.GetBytes(Keyring.Serialize(SigningKit.Keyring(root, 2,
SigningKit.Entry(admin, "Nicolas", SigningScopes.Releases, SigningScopes.Catalog, SigningScopes.Maintenance))));
clock.Now += TimeSpan.FromMinutes(3);
server.Serve(Signed(admin, 1, true, "Retour vers 18 h."));
// Relecture périodique suivante : trousseau retéléchargé, maintenance appliquée.
Assert.True(await service.RefreshAsync());
Assert.True(service.IsActive);
Assert.Equal("Retour vers 18 h.", Assert.Single(changes)!.Message);
}
[Fact]
public async Task Offline_at_startup_or_without_address_there_is_no_maintenance_and_warn_mode_applies_it()
{
+49 -63
View File
@@ -1,98 +1,84 @@
# Launcher 3.3.0
# Launcher 3.3.1
LAUNCHER - NOTES DE MISE À JOUR
Mode maintenance - Mise à jour du 10 novembre 2026
Correctif du mode maintenance - Mise à jour du 10 novembre 2026
==================================================================
Nouvelle page « Maintenance » dans le menu d'administration : un bouton met tous les launchers en mode maintenance. Le launcher passe au rouge / orange, affiche « Maintenance en cours, merci de revenir plus tard » au centre et bloque tout, sauf la désinstallation des jeux et le menu d'administration.
Un launcher resté ouvert pendant la re-signature du trousseau ne voyait pas la maintenance activée ensuite : chaque relecture (toutes les 2 minutes) la refusait jusqu'au redémarrage. Il prend désormais le nouveau trousseau en compte tout seul, et le relit aussi régulièrement quand il reste ouvert longtemps.
Le serveur est à redéployer et un bloc nginx est à ajouter ; le trousseau est à re-signer après la diffusion de cette version.
Aucune action sur le serveur. Le launcher se met à jour automatiquement.
------------------------------------------------------------------
EN BREF
------------------------------------------------------------------
- NOUVEAU : page « Maintenance » (activer, modifier le message,
terminer), permission maintenance.manage.
- NOUVEAU : launcher en rouge / orange, carte « Maintenance en
cours » au centre, bouton « MAINTENANCE ».
- NOUVEAU : tout bloqué chez les joueurs, sauf la désinstallation
des jeux et le menu d'administration.
- AMÉLIORÉ : bandeau au tableau de bord, ligne dans la page
« Signature », catégorie dans le journal d'audit.
- SÉCURITÉ : mode maintenance signé (autorisation « mode
maintenance » du trousseau) et vérifié par les launchers.
- SERVEUR : à redéployer ; bloc nginx maintenance.json à ajouter.
- QUALITÉ : 11 nouveaux tests automatisés (367 au total).
- CORRIGÉ : mode maintenance activé après la re-signature du
trousseau non vu par un launcher déjà ouvert (redémarrage
nécessaire en 3.3.0).
- AMÉLIORÉ : trousseau retéléchargé quand un contenu est signé par
une clé connue mais pas encore autorisée pour ce contenu.
- SÉCURITÉ : trousseau relu au plus toutes les 30 minutes : un
appareil retiré n'est plus accepté par un launcher laissé ouvert.
- QUALITÉ : 3 nouveaux tests automatisés (370 au total).
------------------------------------------------------------------
PAGE « MAINTENANCE »
CORRECTIF
------------------------------------------------------------------
- État publié : en cours depuis quand, par qui ; ou terminée.
- Message facultatif pour les joueurs : 300 caractères, 6 lignes,
texte brut, aucun lien ; compteur, contrôle en direct, aperçu.
- « Activer la maintenance » : confirmation détaillée.
- « Modifier le message » : même heure de début.
- « Terminer la maintenance » : confirmation.
- Le launcher de l'administrateur bascule aussitôt.
- Tableau de bord : bandeau « Maintenance en cours » pour tous les
administrateurs.
- Cause : le launcher gardait en mémoire le trousseau lu au
démarrage. Il ne le retéléchargeait que pour une clé inconnue,
pas pour une clé connue qui vient de recevoir une autorisation
(« mode maintenance » ajoutée à l'étape 3 decies).
- Effet en 3.3.0 : « la clé … n'est pas autorisée à signer ce
contenu » dans le journal à chaque relecture, maintenance
ignorée jusqu'au redémarrage.
- Désormais : nouveau téléchargement du trousseau, au plus une
fois par minute ; la maintenance s'affiche à la relecture
suivante (2 minutes au plus).
- Concerne aussi les formulaires « Aide », le catalogue et les
versions des jeux signés par un appareil qui vient d'être
autorisé.
------------------------------------------------------------------
CHEZ LES JOUEURS
VÉRIFICATIONS PÉRIODIQUES DU LAUNCHER
------------------------------------------------------------------
- Couleurs rouge / orange, carte au centre avec le message et
l'heure de début, fondu au début et à la fin.
- Bloqués : installation, mises à jour (automatiques comprises),
vérification, réparation, localisation, lancement (raccourcis
compris), paramètres, bouton « Aide », site officiel.
- Toujours possibles : désinstallation, menu d'administration.
- Non interrompus : téléchargement commencé, partie en cours, mise
à jour du launcher lui-même.
- Lu au démarrage avant la recherche des mises à jour, puis toutes
les 2 minutes (1 minute pendant une maintenance).
- Hors ligne au démarrage : pas de maintenance (le jeu installé
reste jouable).
- Mode maintenance : au démarrage (4 s au plus), puis toutes les
2 minutes (1 minute pendant une maintenance), et aussitôt après
une publication depuis le menu de ce PC.
- Versions des jeux (manifest) : au démarrage, puis toutes les
30 minutes (updateCheckIntervalMinutes).
- Trousseau : au démarrage ; après un échec réseau (1 minute) ;
clé inconnue ou non autorisée (1 minute au plus) ; au plus
toutes les 30 minutes (nouveau).
- Catalogue : au démarrage seulement.
- Mise à jour du launcher : au démarrage, et bouton « Rechercher »
des paramètres.
- Formulaires « Aide » : à l'ouverture de la fenêtre « Aide ».
------------------------------------------------------------------
SÉCURITÉ
------------------------------------------------------------------
- Publication : permission maintenance.manage, requête signée et
document signé par la clé de la session, audit.
- Launchers : signature vérifiée selon contentSigning ; document
non signé, mal signé ou plus ancien que le dernier vu ignoré en
« require ».
- Aucun lien accepté dans le message ; titre fixe dans le code.
- Affichage pour les joueurs, pas une barrière de sécurité : les
serveurs du jeu restent seuls juges des connexions.
- Chaque trousseau retéléchargé passe par les mêmes contrôles :
signature de la clé racine, numéro jamais en baisse, minimum
keyringMinSequence ; un ancien trousseau resservi est refusé.
- Serveur injoignable : dernier trousseau vérifié conservé.
------------------------------------------------------------------
SERVEUR
------------------------------------------------------------------
- À redéployer : server/deploy/LISEZMOI.md, étapes 8 et 3 decies.
- nginx : bloc location = /catalog/maintenance.json
(server/deploy/nginx-catalog.conf).
- Aucun réglage à ajouter : fichier écrit à côté du trousseau.
- Ordre : serveur et nginx, launcher 3.3.0, puis trousseau re-signé
avec l'autorisation « mode maintenance ».
- Aucun changement.
------------------------------------------------------------------
DÉVELOPPEMENT
------------------------------------------------------------------
- Contrats : MaintenanceDocument, MaintenanceRules,
ContentKinds.Maintenance, SigningScopes.Maintenance,
AdminPermissions.MaintenanceManage, ApiRoutes.Maintenance.
- Serveur : module Maintenance, MaintenanceStore.
- Launcher : MaintenanceService (Core), Themes/Maintenance.xaml,
GameViewModel.IsMaintenance, LauncherViewModel.ApplyMaintenance.
- Menu : MaintenanceAdminModule, MaintenancePanelViewModel.
- Outil ui-snapshot : scènes main-maintenance, main-maintenance-
plain, admin-page-maintenance, admin-page-maintenance-edit.
- Documentation : docs/ADMIN-ARCHITECTURE.md (§ 11 septies).
- Core : ContentTrustService (KeyChangeRetryDelay, KeyringMaxAge,
KeyMayBeNewer).
- Tests : ContentTrustTests (2), MaintenanceServiceTests (1).
- Documentation : README, docs/ADMIN-ARCHITECTURE.md,
server/deploy/LISEZMOI.md (étapes 3 quinquies et 3 decies).
La sécurité des mises à jour du launcher ne change pas : la signature numérique et l'empreinte SHA-256 de chaque nouvelle version sont vérifiées avant toute installation.