Compare commits

...
6 Commits
9 changed files with 203 additions and 119 deletions

No files matched your search

+7
View File
@@ -894,6 +894,13 @@ côté du catalogue, 0644, atomique) et un historique ; audit `keyring.publish`
des administrateurs expose désormais la clé publique des clés **actives** seulement (celle d'une clé en attente
permettrait de calculer l'empreinte à dicter).
**Contenu en ligne** (page Signature, `GET v1/signing` : `SigningState`) : versions en ligne de chaque jeu, catalogue,
puis **formulaires des demandes** (`FeedbackFormsSignatureState` : révision, condensé, signature, signataire ;
révision 0 = formulaires d'origine intégrés au launcher, rien à signer ; fichier de référence illisible signalé).
Chaque contenu est vérifié par le launcher avec le trousseau publié (accepté ou refusé par les joueurs, et pourquoi)
et avec le trousseau proposé (« sa clé ne sera plus dans le trousseau proposé » ; l'export de la demande le rappelle).
Un serveur antérieur à la 3.2.1 n'envoie pas les formulaires : la ligne n'apparaît pas.
**Signature du contenu** (`ContentSigning`) : propriété `signature` `{ keyId, signedAt, value }` dans le JSON
(atomique avec le contenu, ignorée par les anciens launchers), sur le message `GameLauncher/Content/v1`
(type, identifiant du jeu ou « catalog », date à la seconde, clé, condensé). Condensés canoniques, calculés par
@@ -2,6 +2,7 @@ using System.Text.Json;
using GameLauncher.Contracts;
using GameLauncher.Contracts.Catalog;
using GameLauncher.Contracts.Dtos;
using GameLauncher.Contracts.Feedback;
using GameLauncher.Contracts.Security;
using GameLauncher.Contracts.Signing;
using GameLauncher.Server.Hosting;
@@ -13,7 +14,7 @@ namespace GameLauncher.Server.Modules.Signing;
/// <summary>
/// Signature du contenu servi aux joueurs (phase 7) :
/// • GET v1/signing (admins.manage) : trousseau publié, signatures des manifests et du catalogue ;
/// • GET v1/signing (admins.manage) : trousseau publié, signatures des manifests, du catalogue et des formulaires des demandes ;
/// • PUT v1/keyring (admins.manage, requête signée) : publication d'un trousseau signé HORS LIGNE par la clé racine ;
/// • GET / POST v1/games/{gameId}/signature (releases.publish) : signature de la version déjà en ligne.
/// La signature des nouvelles versions et du catalogue se fait à leur publication (modules Publishing et Catalog).
@@ -33,8 +34,8 @@ public sealed class SigningModule : IApiModule
api.MapPost(ApiRoutes.ManifestSignaturePattern, SignManifest).RequirePermission(AdminPermissions.ReleasesPublish).RequireSignedRequest();
}
private static IResult State(KeyringStore keyrings, CatalogStore catalogs, AdminStore admins, IOptions<LauncherApiOptions> options,
ILogger<SigningModule> logger)
private static IResult State(KeyringStore keyrings, CatalogStore catalogs, FeedbackFormsStore forms, AdminStore admins,
IOptions<LauncherApiOptions> options, ILogger<SigningModule> logger)
{
var settings = options.Value;
var db = admins.Read();
@@ -78,6 +79,30 @@ public sealed class SigningModule : IApiModule
};
}
// Formulaires des demandes en vigueur (référence privée, recopiée à l'identique dans le fichier public).
FeedbackFormsSignatureState? feedbackForms = null;
if (forms.PublishingEnabled)
{
try
{
var current = forms.Current;
feedbackForms = current.Revision == 0
? new FeedbackFormsSignatureState()
: new FeedbackFormsSignatureState
{
Revision = current.Revision,
Digest = FeedbackFormRules.Digest(current),
Signature = current.Signature,
SignerName = ContentSignatures.SignerName(db, current.Signature)
};
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException or JsonException)
{
logger.LogWarning(ex, "Signature des formulaires des demandes");
feedbackForms = new FeedbackFormsSignatureState { Unreadable = true };
}
}
KeyringDocument? keyring;
try
{
@@ -97,7 +122,8 @@ public sealed class SigningModule : IApiModule
KeyringPublicUrl = keyrings.PublishingEnabled ? settings.KeyringPublicUrl : null,
RequireSignatures = settings.ContentSigning.Require,
Manifests = manifests,
Catalog = catalog
Catalog = catalog,
FeedbackForms = feedbackForms
}, ApiJson.Options);
}
@@ -55,6 +55,9 @@ public sealed record SigningState
/// <summary>Catalogue publié (null : publication du catalogue non configurée).</summary>
public CatalogSignatureState? Catalog { get; init; }
/// <summary>Formulaires des demandes (null : publication des formulaires non configurée, ou serveur antérieur à la 3.2.1).</summary>
public FeedbackFormsSignatureState? FeedbackForms { get; init; }
}
public sealed record CatalogSignatureState
@@ -64,3 +67,19 @@ public sealed record CatalogSignatureState
public ContentSignature? Signature { get; init; }
public string? SignerName { get; init; }
}
/// <summary>Signature des formulaires des demandes en vigueur (feedback-forms.json).</summary>
public sealed record FeedbackFormsSignatureState
{
/// <summary>Révision en vigueur (0 : formulaires d'origine, intégrés au launcher, jamais publiés).</summary>
public long Revision { get; init; }
/// <summary>Condensé du contenu signé (vide : formulaires d'origine, ou fichier illisible).</summary>
public string Digest { get; init; } = "";
public ContentSignature? Signature { get; init; }
public string? SignerName { get; init; }
/// <summary>Vrai : le fichier de référence des formulaires est illisible sur le serveur.</summary>
public bool Unreadable { get; init; }
}
@@ -9,7 +9,8 @@ namespace GameLauncher.Modules.Admin.Signing;
/// <summary>
/// Page « Signature » (propriétaires) : trousseau publié (vérifié avec la clé racine intégrée à l'exe), trousseau
/// proposé d'après les appareils actifs autorisés à publier, état des signatures du contenu en ligne.
/// proposé d'après les appareils actifs autorisés à publier, état des signatures du contenu en ligne (versions en
/// ligne, catalogue, formulaires des demandes).
/// La clé racine n'est JAMAIS ouverte par le launcher : la demande est exportée, signée hors ligne avec
/// tools\sign-keyring.bat, puis le trousseau signé est publié ici.
/// </summary>
@@ -193,6 +194,16 @@ public sealed class SigningPanelViewModel : AdminPanelViewModel
items.Add(new SignedContentItem($"Catalogue (révision {catalog.Revision})", ContentKinds.Catalog, ContentKinds.CatalogId,
catalog.Digest, catalog.Signature, catalog.SignerName, state.Keyring, proposed,
"Republiez-le depuis la page « Jeux » (il est signé à chaque publication)."));
if (state.FeedbackForms is { } forms)
items.Add(forms.Unreadable
? SignedContentItem.Failed("Formulaires des demandes",
"Fichier des formulaires illisible sur le serveur (voir ses journaux) : restaurez-le depuis feedback-forms-history/ " +
"dans les données privées du serveur.")
: new SignedContentItem(
forms.Revision > 0 ? $"Formulaires des demandes (révision {forms.Revision})" : "Formulaires des demandes",
ContentKinds.FeedbackForms, ContentKinds.FeedbackFormsId, forms.Digest, forms.Signature, forms.SignerName, state.Keyring, proposed,
"Republiez-les depuis la page « Formulaires des demandes » (ils sont signés à chaque publication).",
"Formulaires d'origine, intégrés au launcher : rien à signer tant que rien n'est publié."));
return items;
}
@@ -319,12 +330,12 @@ public sealed class KeyringKeyItem
public sealed class SignedContentItem
{
internal SignedContentItem(string title, string kind, string id, string digest, ContentSignature? signature, string? signerName,
KeyringDocument? published, KeyringDocument proposed, string howToSign)
KeyringDocument? published, KeyringDocument proposed, string howToSign, string nothingPublished = "Rien n'est publié.")
{
Title = title;
if (string.IsNullOrEmpty(digest))
{
Text = "Rien n'est publié.";
Text = nothingPublished;
IsValid = true;
return;
}
@@ -342,6 +353,15 @@ public sealed class SignedContentItem
Text += $" Attention : sa clé ne sera plus dans le trousseau proposé. {howToSign}";
}
private SignedContentItem(string title, string text)
{
Title = title;
Text = text;
}
/// <summary>Contenu dont l'état ne peut pas être lu (refusé, à corriger sur le serveur).</summary>
internal static SignedContentItem Failed(string title, string text) => new(title, text);
public string Title { get; }
public string Text { get; }
+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.2.0</Version>
<Version>3.2.1</Version>
<Authors>Lanisys</Authors>
<Company>LanisysGames</Company>
<Copyright>© LanisysGames</Copyright>
@@ -6,6 +6,7 @@ using GameLauncher.Modules.Admin;
using GameLauncher.Modules.Admin.Publishing;
using GameLauncher.Modules.Admin.Signing;
using GameLauncher.Modules.Admin.ViewModels;
using GameLauncher.Tests.Contracts;
using GameLauncher.Tests.Infrastructure;
using GameLauncher.Tests.Publishing;
using GameLauncher.Tests.Server;
@@ -91,6 +92,70 @@ public class SigningPanelTests
Assert.Contains("déjà publié", page.ErrorMessage);
}
[Fact]
public async Task The_feedback_forms_signature_is_shown_below_the_catalog()
{
var directory = Path.Combine(Path.GetTempPath(), "launcher-signing-tests", Guid.NewGuid().ToString("N"), "catalog");
Directory.CreateDirectory(directory);
await using var api = await TestApi.StartAsync(new Dictionary<string, string?>
{
["LauncherApi:Catalog:PublicFile"] = Path.Combine(directory, "catalog.json"),
["LauncherApi:Games:EZR:ManifestFile"] = ChangelogModuleTests.PublishedManifest(),
["LauncherApi:Feedback:Enabled"] = "true",
["LauncherApi:Feedback:Games:0"] = "EZR"
});
using var folder = new TempFolder();
var (_, signer) = AdminTestKit.CreateKeyFile(folder.Path);
using var _ = signer;
AdminTestKit.RegisterOwner(api, signer);
using var root = SigningKit.NewKey();
// Trousseau n° 1 : la clé de Camille, sans l'autorisation « formulaires ».
using var camilleKey = api.AddOwner("Camille");
await using var camille = await api.LoginAsync(camilleKey);
KeyringKey Entry(params string[] scopes) => new()
{
KeyId = camilleKey.KeyId, PublicKey = Convert.ToBase64String(camilleKey.PublicKey), Name = "Camille — PC", Scopes = scopes.ToList()
};
await camille.PublishKeyringAsync(SigningKit.Keyring(root, 1, Entry(SigningScopes.All.Where(s => s != SigningScopes.Forms).ToArray())));
await using var shell = new AdminShellViewModel(AdminTestKit.Environment(api, folder.Path, new FakeDialogs(), Modules,
rootKeys: new[] { SigningKit.Root(root) }));
await Assert.IsType<LoginViewModel>(shell.Screen).LoginAsync(AdminTestKit.Passphrase);
var workspace = Assert.IsType<AdminWorkspaceViewModel>(shell.Screen);
workspace.Navigate(SigningPanelViewModel.PanelId);
await workspace.Activation;
var page = Assert.IsType<SigningPanelViewModel>(workspace.SelectedPanel!.ViewModel);
// Formulaires d'origine : rien à signer ; affichés après les versions en ligne et le catalogue.
Assert.False(page.HasError, page.ErrorMessage);
Assert.StartsWith("Version en ligne de EZR", page.Contents[0].Title);
Assert.StartsWith("Catalogue", page.Contents[^2].Title);
var forms = page.Contents[^1];
Assert.Equal("Formulaires des demandes", forms.Title);
Assert.StartsWith("Formulaires d'origine", forms.Text);
Assert.True(forms.IsValid);
// Publiés par Camille, dont la clé n'a pas l'autorisation « formulaires » dans le trousseau publié : refusés par
// les launchers ; le trousseau proposé (propriétaire : autorisation « formulaires ») les rendra valides.
await camille.PublishFeedbackFormsAsync(FeedbackFormsKit.Custom(), 0);
await page.RefreshCommand.ExecuteAsync();
forms = page.Contents[^1];
Assert.Equal("Formulaires des demandes (révision 1)", forms.Title);
Assert.False(forms.IsValid);
Assert.False(forms.WillBreak);
Assert.Contains("refusé par les launchers", forms.Text);
Assert.Contains("« Formulaires des demandes »", forms.Text);
// Trousseau n° 2 avec l'autorisation « formulaires » : valides.
await camille.PublishKeyringAsync(SigningKit.Keyring(root, 2, Entry(SigningScopes.All.ToArray())));
await page.RefreshCommand.ExecuteAsync();
forms = page.Contents[^1];
Assert.True(forms.IsValid, forms.Text);
Assert.Contains("Camille", forms.Text);
Assert.Contains("valide pour les launchers", forms.Text);
}
[Fact]
public void Device_names_proposed_for_the_public_keyring_are_cleaned()
{
+8
View File
@@ -709,6 +709,14 @@ internal sealed class SampleWorld
Digest = SampleData.Digest("catalogue"),
Signature = Signature(-3),
SignerName = "Camille — PC-STUDIO"
},
// Formulaires des demandes de démonstration (révision 3, comme la page « Formulaires des demandes »).
FeedbackForms = new FeedbackFormsSignatureState
{
Revision = 3,
Digest = FeedbackFormRules.Digest(SampleData.SampleFeedbackForms()),
Signature = Signature(-1),
SignerName = $"{OwnerName} — PC-BUREAU"
}
};
}
+3 -2
View File
@@ -45,8 +45,9 @@ Les positions du rapport sont en unités WPF (multiplier par `--scale` pour les
démonstration (ni fichier choisi, ni presse-papiers).
* **Menu d'administration** (`admin-*`) : connexion (normale, refusée, verrouillée), « J'ai une
invitation » (formulaire, empreinte), puis **chaque page déclarée par les modules** (`admin-page-<id>`,
lues dans `App.CreateModules` : une page ajoutée plus tard est capturée sans modifier l'outil), une
version prête à publier, la fiche d'une demande de joueur (`admin-page-feedback-requests-detail` :
lues dans `App.CreateModules` : une page ajoutée plus tard est capturée sans modifier l'outil ; la page
« Signature », `admin-page-signing`, montre les versions en ligne, le catalogue et les formulaires des
demandes), une version prête à publier, la fiche d'une demande de joueur (`admin-page-feedback-requests-detail` :
lien signalé, captures d'écran) et un formulaire des demandes en cours de modification
(`admin-page-feedback-forms-edit` : champ ajouté et déplié, erreur « lien » et avertissement « donnée
personnelle », aperçu tel que les joueurs le verront), puis un type de demande ajouté
+47 -109
View File
@@ -1,140 +1,78 @@
# Launcher 3.2.0
# Launcher 3.2.1
LAUNCHER - NOTES DE MISE À JOUR
Formulaires des demandes modifiables - Mise à jour du 10 novembre 2026
Signature des formulaires des demandes - Mise à jour du 10 novembre 2026
==================================================================
Les formulaires du bouton « Aide » se gèrent désormais depuis le menu d'administration : de 1 à 8 types de demande (ajout, suppression, ordre d'affichage, icône, captures d'écran autorisées ou non) et leurs champs, avec un aperçu identique à la fenêtre des joueurs.
La page « Signature » du menu d'administration affiche désormais l'état de signature des formulaires des demandes, sous les versions en ligne et le catalogue : formulaires d'origine, signés et valides, ou refusés par les launchers avec la raison et le remède.
Les formulaires sont communs à tous les jeux, signés et vérifiés par le launcher, et ne peuvent contenir aucun lien. Le serveur est à redéployer, avec deux blocs nginx à ajouter ; le trousseau est à re-signer une fois cette version diffusée.
Le serveur est à redéployer ; aucun bloc nginx à ajouter.
------------------------------------------------------------------
EN BREF
------------------------------------------------------------------
- NOUVEAU : page « Formulaires des demandes » dans le menu :
types de demande, champs, aperçu, contrôle en direct, publication
signée.
- NOUVEAU : 1 à 8 types de demande : ajout, suppression, ordre
d'affichage, type masqué.
- NOUVEAU : icône d'un type : l'une des quatre d'origine, ou un
fichier .ico importé (image 64 × 64 obligatoire).
- NOUVEAU : captures d'écran autorisées ou non par type de demande.
- NOUVEAU : champs texte court, cases à cocher, case oui/non, notes
de 3, 5 ou 10 étoiles.
- AMÉLIORÉ : fenêtre « Aide » : formulaires publiés, saisie gardée
si les formulaires changent pendant la saisie.
- AMÉLIORÉ : chaque demande garde le formulaire de son envoi (nom,
icône, libellés).
- SÉCURITÉ : formulaires signés et revérifiés par le launcher ;
aucun lien accepté dans les textes.
- SERVEUR : à redéployer, avec deux blocs nginx ; trousseau à
re-signer après la diffusion de la 3.2.0.
- QUALITÉ : 13 nouveaux tests automatisés (355 au total).
- AMÉLIORÉ : page « Signature » : ligne « Formulaires des demandes »
dans « Contenu en ligne », sous les versions en ligne et le
catalogue.
- AMÉLIORÉ : vérification avec le trousseau publié (accepté ou refusé
par les joueurs, et pourquoi) et avec le trousseau proposé.
- SÉCURITÉ : export de la demande de trousseau : les formulaires qui
deviendraient invalides sont rappelés.
- SERVEUR : à redéployer ; aucun bloc nginx à ajouter.
- QUALITÉ : 1 nouveau test automatisé (356 au total).
------------------------------------------------------------------
PAGE « FORMULAIRES DES DEMANDES » (ADMINISTRATION)
PAGE « SIGNATURE »
------------------------------------------------------------------
- Permission « Formulaires des demandes » (feedback.forms).
- Types de demande : ajouter (8 au plus), monter, descendre,
supprimer (avec confirmation ; les demandes reçues restent
consultables), masquer.
- Réglages d'un type : nom, description, exemple de titre,
informations techniques cochées par défaut, captures d'écran
autorisées (3 au plus), icône.
- Champs : ajouter, monter, descendre, retirer ; libellé, aide,
obligatoire, aperçu dans la liste, longueur, nombre d'étoiles,
choix (2 à 20).
- Types de champs : zone de texte, texte court, liste déroulante,
cases à cocher, case oui/non, étoiles (3, 5 ou 10).
- Aperçu identique à la fenêtre « Aide » ; contrôle en direct, la
publication reste bloquée tant qu'il reste une erreur.
- Avertissement si un libellé semble demander une donnée
personnelle (e-mail, mot de passe, téléphone…).
- « Publier », « Annuler les modifications », « Revenir à
l'origine », « Recharger » ; une publication d'un autre
administrateur n'est jamais écrasée.
------------------------------------------------------------------
ICÔNES IMPORTÉES
------------------------------------------------------------------
- Fichier .ico seulement (1 Mo au plus), contenant une image de
64 × 64 pixels ; les autres tailles du fichier sont ignorées.
- PNG intégré ou bitmap 24/32 bits ; 256 couleurs et moins refusés.
- Image réécrite en PNG neuf, sans métadonnées, intégrée aux
formulaires et couverte par leur signature.
- Un fichier refusé affiche un message qui explique pourquoi.
------------------------------------------------------------------
FENÊTRE « AIDE » ET DEMANDES DES JOUEURS
------------------------------------------------------------------
- Formulaires publiés lus à l'ouverture de la fenêtre, puis
vérifiés (règles, signature, révision).
- Types dans l'ordre choisi, avec leur icône ; types masqués
absents ; section « Captures d'écran » masquée si le type ne les
autorise pas.
- Formulaires republiés pendant la saisie : formulaire reconstruit,
saisie des champs inchangés gardée.
- Hors ligne ou formulaires refusés : formulaires d'origine.
- Menu : chaque demande s'affiche avec le formulaire de son envoi ;
filtre par type publié, « (masqué) » ou « (supprimé) ».
- Journal d'audit : « Formulaires des demandes publiés », avec le
détail des changements par type.
- Contenu en ligne, dans l'ordre : versions en ligne, catalogue,
formulaires des demandes.
- Formulaires d'origine : « rien à signer tant que rien n'est
publié ».
- Formulaires publiés : révision, signataire et date ; « valide pour
les launchers », ou refus avec la raison (clé absente du trousseau,
autorisation « formulaires » manquante…) et le remède (republier
depuis la page « Formulaires des demandes »).
- Avertissement si la clé qui les a signés ne sera plus dans le
trousseau proposé ; rappel à l'export de la demande.
- Fichier des formulaires illisible sur le serveur : signalé, avec
la marche à suivre (historique feedback-forms-history/).
- Serveur antérieur à la 3.2.1 : la ligne n'apparaît pas.
------------------------------------------------------------------
SÉCURITÉ
------------------------------------------------------------------
- Formulaires signés par une clé autorisée « formulaires » dans le
trousseau ; vérifiés par chaque launcher, comme le catalogue.
- Aucun lien dans les noms, descriptions, libellés, aides et choix :
refusé par le menu, le serveur et le launcher.
- Ancienne révision refusée par le launcher.
- Publication par requête signée, avec la permission
feedback.forms ; révision attendue (rien n'est écrasé).
- Taille limitée : 256 Ko de formulaires, icônes comprises.
- Le serveur refuse une demande avec captures pour un type qui ne
les autorise pas, et une demande pour un type masqué ou supprimé.
- Une autorisation inconnue du trousseau est désormais acceptée sans
effet.
- Accès inchangé : session et permission admins.manage.
- Vérification faite par le launcher avec le trousseau publié et la
clé racine intégrée, comme chez les joueurs.
- Rien de secret renvoyé : révision, condensé, signature et
signataire de formulaires publics.
------------------------------------------------------------------
SERVEUR
------------------------------------------------------------------
- À redéployer : server/deploy/LISEZMOI.md, étape 8, puis
étape 3 nonies.
- nginx : ajouter « location = /catalog/feedback-forms.json »
(nginx-catalog.conf) et « location = /api/v1/feedback/forms »
(nginx-api.conf, 300 Ko).
- Publier ensuite le launcher 3.2.0, puis donner la permission
feedback.forms et re-signer le trousseau.
- Les launchers 3.0 / 3.1 envoient toujours leurs demandes avec les
formulaires d'origine, acceptées sauf type masqué ou supprimé.
- À redéployer : server/deploy/LISEZMOI.md, étape 8.
- Aucun réglage ni bloc nginx à ajouter.
- Ordre indifférent entre serveur et launcher.
- Après la mise à jour, vérifier dans la page « Signature » que les
formulaires publiés sont valides pour les launchers ; sinon,
re-signer le trousseau avec l'autorisation « formulaires des
demandes ».
------------------------------------------------------------------
DÉVELOPPEMENT
------------------------------------------------------------------
- Contrats : FeedbackFormsDocument (document signé et règles),
FeedbackIcons (lecture des .ico, sans bibliothèque).
- Signature : type de contenu feedback-forms, autorisation « forms »
du trousseau.
- Serveur : FeedbackFormsStore (référence privée, historique de 20
publications, copie publique) ; route v1/feedback/forms.
- Launcher : FeedbackFormsService (lecture et vérification),
Themes/FeedbackForms.xaml (modèles partagés avec l'aperçu).
- Menu : FeedbackFormsPanelViewModel, FeedbackFormEditors.
- Outil ui-snapshot : scènes admin-page-feedback-forms-edit,
admin-page-feedback-forms-new-type, feedback-custom-kinds et
feedback-custom.
- Tests : FeedbackFormsTests.cs, FeedbackFormsModuleTests.cs,
FeedbackFormsPanelTests.cs (nouveaux), ContentSigningTests.cs,
FeedbackPanelTests.cs.
- Documentation : README.md, docs/ADMIN-ARCHITECTURE.md
(§ 11 sexies) et server/deploy/LISEZMOI.md mis à jour.
- Contrats : FeedbackFormsSignatureState, SigningState.FeedbackForms.
- Serveur : GET v1/signing renvoie la signature des formulaires en
vigueur (SigningModule).
- Menu : SigningPanelViewModel (ligne « Formulaires des demandes »).
- Outil ui-snapshot : formulaires de démonstration dans la scène
admin-page-signing.
- Tests : SigningPanelTests.cs.
- Documentation : docs/ADMIN-ARCHITECTURE.md (§ 12) mis à jour.
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.