Un serveur de signature conserve la clé privée à un seul endroit, et c'est précisément là tout l'intérêt d'en avoir un. Le prix habituel à payer est le fichier lui-même : l'agent de compilation envoie l'installateur, le serveur le signe et renvoie l'ensemble. Pour un installateur de plusieurs gigaoctets, cela revient à faire traverser le réseau deux fois à l'installateur, pour ajouter une signature de quelques kilooctets.
Cela n'a pas à être ainsi. Ce qu'Authenticode signe n'est jamais le fichier, mais une empreinte du fichier, et cette empreinte peut être calculée là où le fichier se trouve déjà. Depuis sgcSign 2026.10, le sgcSign Server signe un package Windows Installer ainsi qu'un package MSIX ou APPX à partir de cette seule empreinte, comme il le faisait déjà pour un EXE.
Un installateur de 1 GB signé deux fois par le sgcSign Server, une fois en le téléversant et une fois à partir de son hachage, avec chaque octet compté sur le réseau. Également sur YouTube.
Ce qui est réellement signé
Une signature Authenticode couvre une empreinte, et chaque format définit la sienne. Pour un EXE, c'est le hachage de l'image PE, sans le checksum ni la table des certificats. Pour un MSI, c'est une empreinte calculée sur les streams du compound file dans un ordre fixe. Pour un MSIX, c'est un petit bloc d'empreintes SHA-256 sur les parties du package. Le serveur n'a besoin que de cette valeur, rien d'autre : il signe l'empreinte avec la clé qu'il détient, et le client écrit la signature dans le fichier.
Depuis la ligne de commande
L'outil en ligne de commande prend par défaut la route de hachage pour --format msi et --format appx, comme il le faisait déjà pour --format authenticode, et --upload envoie le fichier entier à la place. La démonstration signe le même package de 1 GB des deux façons contre un serveur situé sur la même machine, via un petit relais qui compte chaque octet envoyé et reçu par l'outil. Le package contient 1 GiB de données aléatoires, si bien que rien en chemin ne peut le compresser.
> sgcsign sign --format msi --upload --server http://127.0.0.1:18481 --apikey $k `
--provider demo --tsa http://timestamp.digicert.com --verbose `
--out BigApp-signed-upload.msi BigApp.msi
Signed: BigApp-signed-upload.msi | Signer: O=eSeGeCe Demo, CN=sgcSign Demo Code Signing | Duration: 3015 ms
> Get-Content .\relay.log -Tail 1
RUN 4: connections=1 bytes_up(client->server)=1075356521 bytes_down(server->client)=1075364108 wall=14.087s
> sgcsign sign --format msi --server http://127.0.0.1:18481 --apikey $k `
--provider demo --tsa http://timestamp.digicert.com --verbose `
--out BigApp-signed-hash.msi BigApp.msi
[prehash] alg=sha256 hash=9151f2ee34b18f80a20d4b2cec515ac344066bbed846c7efbc98d7c5e5a0cddd
> Get-Content .\relay.log -Tail 1
RUN 5: connections=1 bytes_up(client->server)=477 bytes_down(server->client)=11044 wall=0.178s
Les deux fichiers reviennent signés et portent la même empreinte : signtool verify /v affiche dans les deux cas Hash of file (sha256): 9151F2EE…5A0CDDD, la valeur de la ligne [prehash], avec le signataire de démonstration et l'horodatage DigiCert.
Les chiffres
| Route d'envoi | Route de hachage | |
|---|---|---|
| Package | 1 075 355 648 bytes | 1 075 355 648 bytes |
| Envoyé au serveur | 1 075 356 521 bytes | 477 bytes |
| Reçu du serveur | 1 075 364 108 bytes | 11 044 bytes |
| Temps sur le réseau | 14,1 s | 0,18 s |
| Commande complète | 16,3 s | 4,8 s |
Ceci s'est exécuté en loopback, où déplacer des octets ne coûte presque rien, et la route d'envoi y a tout de même passé 14 secondes. Sur une liaison réelle, le transfert représente tout le coût : à 100 Mbit/s, les 2,15 GB de la route d'envoi prennent près de trois minutes, alors que la route de hachage envoie toujours 477 octets. Ce qu'il reste des 4,8 secondes de la route de hachage, c'est la préparation et le hachage du package de 1 GB sur l'agent de compilation, un travail que la route d'envoi effectue aussi, mais sur le serveur.
La même chose en Delphi
Dans un outil de build Delphi, le déroulement tient en quatre étapes : préparer le package, hacher le package préparé, envoyer le hachage et insérer la signature reçue en retour. L'appel HTTP se fait avec le client déjà utilisé par votre projet. Celui-ci utilise THTTPClient et System.JSON de la RTL.
program HashSignMSI;
{$APPTYPE CONSOLE}
uses
System.SysUtils, System.Classes, System.JSON, System.Net.HttpClient,
sgcSign_MSI, sgcSign_Base64, sgcSign_Authenticode;
const
CS_SERVER = 'http://127.0.0.1:18480';
var
vPrepared, vSigned, vHex, vBody: string;
vHash, vPKCS7: TBytes;
vI: Integer;
oHTTP: THTTPClient;
oFields: TStringList;
oResponse: IHTTPResponse;
oJSON: TJSONValue;
begin
try
vPrepared := ChangeFileExt(ParamStr(1), '.prepared.msi');
vSigned := ChangeFileExt(ParamStr(1), '.signed.msi');
// 1. Prepare: this is the package the digest covers
sgcMSIPrepareForSigning(ParamStr(1), vPrepared);
// 2. Hash the PREPARED package on this machine
vHash := sgcMSIComputeHash(vPrepared, ahSHA256);
vHex := '';
for vI := 0 to Length(vHash) - 1 do
vHex := vHex + LowerCase(IntToHex(vHash[vI], 2));
Writeln('SHA-256 : ', vHex);
// 3. Only the digest leaves the machine
oHTTP := THTTPClient.Create;
oFields := TStringList.Create;
try
oHTTP.CustomHeaders['X-API-Key'] := GetEnvironmentVariable('SGCSIGN_APIKEY');
oFields.Add('hash=' + vHex);
oFields.Add('alg=sha256');
oFields.Add('provider=demo');
oFields.Add('tsa_url=http://timestamp.digicert.com');
oFields.Add('level=t');
oResponse := oHTTP.Post(CS_SERVER + '/api/v1/sign/msi/hash', oFields);
vBody := oResponse.ContentAsString(TEncoding.UTF8);
if oResponse.StatusCode <> 200 then
raise Exception.CreateFmt('HTTP %d: %s', [oResponse.StatusCode, vBody]);
finally
oFields.Free;
oHTTP.Free;
end;
// 4. Base64-decode the PKCS#7 and embed it into the SAME prepared package
oJSON := TJSONObject.ParseJSONValue(vBody);
try
vPKCS7 := TsgcBase64.Decode(oJSON.GetValue<string>('signature'));
finally
oJSON.Free;
end;
sgcMSIEmbedSignature(vPrepared, vSigned, vPKCS7);
Writeln('PKCS#7 : ', Length(vPKCS7), ' bytes');
Writeln('Signed : ', vSigned);
except
on E: Exception do
begin
Writeln(ErrOutput, 'error: ', E.Message);
ExitCode := 1;
end;
end;
end.
Contre le même serveur, la réponse a renvoyé un PKCS#7 de 7 941 octets, et signtool lit le résultat exactement comme pour les deux fichiers ci-dessus.
Préparer d'abord, hacher ensuite
Un package d'installation est signé dans la forme sous laquelle il sera livré, il doit donc être mis sous cette forme avant d'être haché. sgcMSIPrepareForSigning et sgcAppxPrepareForSigning s'en chargent, et la règle est simple : hachez ce que prepare a produit, et insérez la signature dans ce même package préparé, pas dans l'original. Hachez l'original à la place, et la signature couvrira des octets qui n'existent plus.
MSIX : vérifiez vous-même le publisher
Sur la route de hachage, aucun des deux côtés ne voit les deux moitiés de la seule vérification sur laquelle Windows est strict. Le serveur détient le certificat et ne voit jamais le manifeste, et le client détient le manifeste et ne voit jamais le certificat. C'est pourquoi chaque réponse de hachage porte signer_subject, le subject du certificat qui a signé. Comparez-le au Publisher du package, avec sgcAppxReadPublisher et sgcAppxDNMatches, avant d'insérer la signature. L'outil en ligne de commande le fait tout seul et refuse d'écrire le package quand ils diffèrent.
Quand envoyer le fichier malgré tout
Les routes en fichier complet restent disponibles. Utilisez-les quand le client ne peut pas calculer lui-même l'empreinte, ou quand une signature nécessite une approbation, car le circuit d'approbation ne fonctionne qu'avec des envois en fichier complet. --upload fait revenir l'outil en ligne de commande sur cette route pour une seule exécution.
Disponibilité
Les routes de hachage pour les packages d'installation, /api/v1/sign/msi/hash et /api/v1/sign/appx/hash, le comportement par défaut de hachage d'abord de l'outil en ligne de commande, ainsi que les fonctions de préparation, de hachage et d'insertion, sont disponibles dans sgcSign 2026.10 pour Delphi et C++Builder. Ces routes sont documentées dans l'aide en ligne de sgcSign.
Vous signez de gros packages depuis une ferme de build ? Contactez-nous, et vous recevrez une réponse de ceux qui ont écrit le code.
