Un server di firma tiene la chiave privata in un unico posto, ed è proprio questo il senso di averne uno. Il prezzo abituale è il file stesso: l'agente di build carica l'installer, il server lo firma e rimanda indietro il file intero. Per un installer di alcuni gigabyte questo significa far attraversare la rete due volte all'installer, solo per aggiungere una firma di pochi kilobyte.
Non deve essere per forza così. Ciò che Authenticode firma non è mai il file, bensì un digest del file, e quel digest può essere calcolato dove il file già si trova. A partire da sgcSign 2026.10, il sgcSign Server firma un pacchetto Windows Installer e un pacchetto MSIX o APPX a partire da quel solo digest, così come già faceva per un EXE.
Un installer da 1 GB firmato due volte tramite il sgcSign Server, una volta caricandolo e una a partire dal suo hash, contando ogni byte sulla rete. Anche su YouTube.
Cosa viene realmente firmato
Una firma Authenticode copre un digest, e ogni formato definisce il proprio. Per un EXE è l'hash dell'immagine PE, escludendo il checksum e la tabella dei certificati. Per un MSI è un digest calcolato sugli stream del compound file in un ordine fisso. Per un MSIX è un piccolo blocco di digest SHA-256 sulle parti del pacchetto. Il server ha bisogno solo di quel valore e di nient'altro: firma il digest con la chiave che possiede, e il client scrive la firma nel file.
Dalla riga di comando
Lo strumento a riga di comando usa di default la route hash per --format msi e --format appx, come già faceva per --format authenticode, e --upload invia invece il file intero. La demo firma lo stesso pacchetto da 1 GB in entrambi i modi contro un server sulla stessa macchina, tramite un piccolo relay che conta ogni byte inviato e ricevuto dallo strumento. Il pacchetto contiene 1 GiB di dati casuali, così che nulla lungo il percorso possa comprimerlo.
> 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
Entrambi i file tornano firmati e portano lo stesso digest: signtool verify /v mostra in entrambi Hash of file (sha256): 9151F2EE…5A0CDDD, il valore della riga [prehash], con il firmatario demo e il timestamp DigiCert.
I numeri
| Route di upload | Route di hash | |
|---|---|---|
| Pacchetto | 1.075.355.648 bytes | 1.075.355.648 bytes |
| Inviato al server | 1.075.356.521 bytes | 477 bytes |
| Ricevuto dal server | 1.075.364.108 bytes | 11.044 bytes |
| Tempo sulla rete | 14,1 s | 0,18 s |
| Comando completo | 16,3 s | 4,8 s |
Questo test è stato eseguito su loopback, dove spostare byte non costa quasi nulla, eppure la route di upload ci ha impiegato 14 secondi. Su un collegamento reale, il trasferimento è tutto il costo: a 100 Mbit/s, i 2,15 GB della route di upload richiedono quasi tre minuti, mentre la route di hash invia comunque solo 477 byte. Ciò che resta dei 4,8 secondi della route di hash è la preparazione e il calcolo dell'hash del pacchetto da 1 GB sull'agente di build, un lavoro che anche la route di upload svolge, solo che lo fa sul server.
La stessa cosa in Delphi
In uno strumento di build Delphi il flusso è di quattro passaggi: preparare il pacchetto, calcolare l'hash del pacchetto preparato, inviare l'hash e incorporare la firma che torna indietro. La chiamata HTTP è quella che il vostro progetto usa già. Questo esempio usa THTTPClient e System.JSON della 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.
Contro lo stesso server, la risposta ha restituito un PKCS#7 di 7.941 byte, e signtool legge il risultato esattamente come nei due file sopra.
Prima prepara, poi calcola l'hash
Un pacchetto installer viene firmato nella forma in cui verrà distribuito, quindi va portato in quella forma prima di calcolarne l'hash. sgcMSIPrepareForSigning e sgcAppxPrepareForSigning fanno questo, e la regola è semplice: calcolate l'hash di ciò che prepare ha restituito, e incorporate la firma in quello stesso pacchetto preparato, non nell'originale. Calcolate l'hash dell'originale, invece, e la firma coprirà byte che non esistono più.
MSIX: verificate voi stessi il publisher
Sulla route di hash, nessuno dei due lati vede entrambe le metà dell'unico controllo su cui Windows è rigoroso. Il server possiede il certificato e non vede mai il manifest, e il client possiede il manifest e non vede mai il certificato. Per questo ogni risposta hash porta signer_subject, il subject del certificato che ha firmato. Confrontatelo con il Publisher del pacchetto, con sgcAppxReadPublisher e sgcAppxDNMatches, prima di incorporare la firma. Lo strumento a riga di comando lo fa da solo e rifiuta di scrivere il pacchetto quando i due differiscono.
Quando caricare comunque il file
Le route a file intero restano disponibili. Usatele quando il client non può calcolare da solo il digest, o quando una firma richiede approvazione, perché il flusso di approvazione funziona solo con upload a file intero. --upload riporta lo strumento a riga di comando su quella route per una singola esecuzione.
Disponibilità
Le route hash per i pacchetti installer, /api/v1/sign/msi/hash e /api/v1/sign/appx/hash, il comportamento predefinito hash first dello strumento a riga di comando e le funzioni di preparazione, hash e incorporamento sono disponibili in sgcSign 2026.10 per Delphi e C++Builder. Le route sono documentate nella guida online di sgcSign.
State firmando pacchetti grandi da una build farm? Contattateci, e riceverete una risposta da chi ha scritto il codice.
