Um servidor de assinatura mantém a chave privada em um único lugar, e esse é o motivo de existir. O preço costuma ser o próprio arquivo: o agente de build faz upload do instalador, o servidor o assina e devolve o pacote inteiro. Para um instalador de alguns gigabytes, isso significa o instalador atravessando a rede duas vezes, só para acrescentar uma assinatura de alguns kilobytes.
Não precisa ser assim. O que o Authenticode assina nunca é o arquivo, é um digest do arquivo, e esse digest pode ser calculado onde o arquivo já está. A partir do sgcSign 2026.10, o sgcSign Server assina um pacote Windows Installer e um pacote MSIX ou APPX apenas a partir desse digest, do mesmo jeito que já fazia para um EXE.
Um instalador de 1 GB assinado duas vezes pelo sgcSign Server, uma vez com upload e outra a partir do seu hash, com cada byte na rede contabilizado. Também no YouTube.
O que realmente é assinado
Uma assinatura Authenticode cobre um digest, e cada formato define o seu. Para um EXE é o hash da imagem PE, sem o checksum e a tabela de certificados. Para um MSI é um digest sobre os streams do compound file em uma ordem fixa. Para um MSIX é um pequeno bloco de digests SHA-256 sobre as partes do pacote. O servidor precisa apenas desse valor: ele assina o digest com a chave que possui, e o cliente grava a assinatura no arquivo.
Pela linha de comando
A ferramenta de linha de comando usa a rota de hash por padrão para --format msi e --format appx, como já fazia para --format authenticode, e --upload envia o arquivo inteiro em vez disso. A demonstração assina o mesmo pacote de 1 GB das duas formas contra um servidor na mesma máquina, através de um pequeno relay que conta cada byte que a ferramenta envia e recebe. O pacote contém 1 GiB de dados aleatórios, de modo que nada no caminho consegue comprimi-lo.
> 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
Os dois arquivos voltam assinados, e carregam o mesmo digest: signtool verify /v mostra Hash of file (sha256): 9151F2EE…5A0CDDD em ambos, o valor da linha [prehash], com o assinante de demonstração e o timestamp da DigiCert.
Os números
| Rota de upload | Rota de hash | |
|---|---|---|
| Pacote | 1.075.355.648 bytes | 1.075.355.648 bytes |
| Enviado ao servidor | 1.075.356.521 bytes | 477 bytes |
| Recebido do servidor | 1.075.364.108 bytes | 11.044 bytes |
| Tempo na rede | 14,1 s | 0,18 s |
| Comando completo | 16,3 s | 4,8 s |
Isso rodou em loopback, onde mover bytes custa quase nada, e mesmo assim a rota de upload gastou 14 segundos nisso. Em um link real, a transferência é o custo inteiro: a 100 Mbit/s, os 2,15 GB da rota de upload levam quase três minutos, e a rota de hash ainda envia 477 bytes. O que resta dos 4,8 segundos da rota de hash é preparar e calcular o hash do pacote de 1 GB no agente de build, trabalho que a rota de upload também faz, só que no servidor.
A mesma coisa em Delphi
Em uma ferramenta de build em Delphi, o fluxo tem quatro etapas: preparar o pacote, calcular o hash do pacote preparado, enviar o hash e incorporar a assinatura recebida. A chamada HTTP é feita com o cliente que o seu projeto já usa. Este exemplo usa o THTTPClient e o System.JSON da 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.
No mesmo servidor, a resposta trouxe um PKCS#7 de 7.941 bytes, e o signtool lê o resultado exatamente como nos dois arquivos acima.
Prepare primeiro, depois calcule o hash
Um pacote de instalador é assinado na forma em que será distribuído, por isso precisa ser colocado nessa forma antes de ter o hash calculado. sgcMSIPrepareForSigning e sgcAppxPrepareForSigning fazem isso, e a regra é simples: calcule o hash do que o prepare devolveu, e incorpore a assinatura nesse mesmo pacote preparado, não no original. Calcule o hash do original e a assinatura vai cobrir bytes que já não existem.
MSIX: verifique o publisher você mesmo
Na rota de hash, nenhum dos dois lados vê as duas metades da verificação com a qual o Windows é rigoroso. O servidor guarda o certificado e nunca vê o manifesto, e o cliente guarda o manifesto e nunca vê o certificado. Por isso, toda resposta de hash traz o signer_subject, o subject do certificado que assinou. Compare com o Publisher do pacote, usando sgcAppxReadPublisher e sgcAppxDNMatches, antes de incorporar a assinatura. A ferramenta de linha de comando faz isso sozinha e se recusa a gravar o pacote quando eles diferem.
Quando fazer upload mesmo assim
As rotas de arquivo completo continuam disponíveis. Use-as quando o cliente não conseguir calcular o digest sozinho, ou quando uma assinatura precisar de aprovação, porque o fluxo de aprovação só funciona com uploads de arquivo completo. --upload muda a ferramenta de linha de comando de volta para essa rota em uma única execução.
Disponibilidade
As rotas de hash para pacotes de instalador, /api/v1/sign/msi/hash e /api/v1/sign/appx/hash, o padrão de hash primeiro da ferramenta de linha de comando e as funções de preparar, calcular hash e incorporar estão disponíveis no sgcSign 2026.10 para Delphi e C++Builder. As rotas estão documentadas na ajuda online do sgcSign.
Está assinando pacotes grandes a partir de um build farm? Entre em contato, e você receberá uma resposta de quem escreveu o código.
